Home » Customization

Category Archives: Customization

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. πŸ˜‰

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.

Alter the Appearance of the Sitecore Content Tree using a Pipeline-backed MasterDataView

About 4 years ago, I blogged about creating a custom MasterDataView class to change the appearance of Items in the content tree.

One thing that bugged me back then was how this particular piece of Sitecore functionality wasn’t backed by a pipeline of some sort which gave me an idea on creating a custom MasterDataView which would delegate to a custom pipeline to alter how Items display in the content tree but I never got to it; as a matter of fact, I forgot all about it once I moved to Australia at the time — it’s easy to forget things when you are trying to dodge viscious magpies and venomous spiders πŸ˜‰

However, I recently rethought of this idea due to a project I had worked on where I had to notify content authors that they had almost or too many child items in Media Folders in the Media Library. In this post, I am going to share this solution with you, and it heavily uses code from my previous post — I suggest having a read of that before proceeding.

I also want to call out that I’ve omitted code which registers most services used on this post in the Sitecore IoC container for brevity. If you want to learn about Dependency Injection on Sitecore, I highly recommend watching this presentation on YouTube.

Like any Sitecore pipeline, we need a PipelineArgs class of some sort, and the following is just that:

using Sitecore.Collections;
using Sitecore.Data.Items;
using Sitecore.Pipelines;

namespace Foundation.Kernel.Pipelines.DataViewChildItems
{
	public class DataViewChildItemsArgs : PipelineArgs
	{
		public ItemCollection Children { get; set; }

		public Item Parent { get; set; }

		public DataViewChildItemsArgs()
		{
		}

		public DataViewChildItemsArgs(ItemCollection children, Item parent)
		{
			Children = children;
			Parent = parent;
		}
	}
}

The GetChildItems() method on Sitecore.Web.UI.HtmlControls.MasterDataView, or those which inherit from it, take in an ItemCollection of children and their parent Item in the content tree. This is why I have these two properties on the DataViewChildItemsArgs class above — we are going to pass these to all processors of our custom pipeline.

On my recent project, I wanted to give the ability to have things be turned on and off via a feature toggle in Sitecore configuration, both at a global and invidual piece functionality level (i.e. have a global settings object with an Enabled property but also another Enabled property on each pipeline processor so we can turn the entire thing off, or just a piece of it).

The follow interface is to go with each bit of functionality that can be turned on/off:

namespace Foundation.Kernel.Services.FeatureToggle
{
	public interface IFeatureToggleable
	{
		bool Enabled { get; set; }
	}
}

We also need a service to read these Enabled properties, and determine if something should be turned on/off. The following is the interface for this service:

namespace Foundation.Kernel.Services.FeatureToggle
{
	public interface IFeatureToggleService
	{
		bool IsEnabled(params IFeatureToggleable[] toggleables);
	}
}

Here’s the implementation of the interface above:

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

namespace Foundation.Kernel.Services.FeatureToggle
{
	public class FeatureToggleService : IFeatureToggleService
	{
		public bool IsEnabled(params IFeatureToggleable[] toggleables) => !ShouldTurnOff(toggleables);

		protected virtual bool ShouldTurnOff(IEnumerable<IFeatureToggleable> toggleables)
		{
			if (toggleables == null || !toggleables.Any())
			{
				return true;
			}

			return toggleables.Any(toggleable => !toggleable.Enabled);
		}
	}
}

It takes in a collection of IFeatureToggleable, and returns false if any of the IFeatureToggleable has an Enabled property value of “false”.

I then define a base class for pipeline processors of the custom pipeline:

using Foundation.Kernel.Services.FeatureToggle;

namespace Foundation.Kernel.Pipelines.DataViewChildItems
{
	public abstract class DataViewChildItemsProcessor : IFeatureToggleable
	{
		private readonly IFeatureToggleService _featureToggleService;

		public bool Enabled { get; set; }

		protected bool AbortPipelineAfterExecution { get; set; }

		protected DataViewChildItemsProcessor(IFeatureToggleService featureToggleService)
		{
			_featureToggleService = featureToggleService;
		}

		public void Process(DataViewChildItemsArgs args)
		{
			if (!IsEnabled() || !ShouldExecute(args))
			{
				return;
			}

			GetChildItems(args);
			if (!ShouldAbortPipeline(args))
			{
				return;
			}

			args.AbortPipeline();
		}

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

		protected abstract bool ShouldExecute(DataViewChildItemsArgs args);

		protected abstract void GetChildItems(DataViewChildItemsArgs args);

		protected virtual bool ShouldAbortPipeline(DataViewChildItemsArgs args) => AbortPipelineAfterExecution;
	}
}

Any pipeline processor which implements the base class above must consume the IFeatureToggleService instance above — ideally from the Sitecore IoC container which I have done further down below but you could also use Poor Man’s DI for this which I do not recommend as we have a native IoC container in Sitecore πŸ˜‰

Now, let’s dive into setting up the custom MasterDataView. The following Config Object are settings for this class, and it contains values for both the custom pipeline’s domain and the pipeline’s name (I decided to group this by pipeline domain as I can see entry points on other overridable methods on the MasterDataView class which could be driven by other custom pipelines in this domain):

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

namespace Foundation.Kernel.Models.DataViews
{
	[ServiceConfigObject(ConfigPath = "moduleSettings/foundation/kernel/pipelineDrivenDataViewSettings", Lifetime = Lifetime.Singleton)]
	public class PipelineDrivenDataViewSettings
	{
		public string PipelineDomain { get; set; }

		public string PipelineName { get; set;  }
	}
}

I then created the following MasterDataView which inherits from the OOTB Sitecore.Buckets.Forms.BucketDataView in Sitecore.Buckets.dll as the OOTB MasterDataView which ships with Sitecore is this one — I could have gone ahead and created yet another pipeline processor for Buckets but I decided against this for now:

using Sitecore.Abstractions;
using Sitecore.Buckets.Forms;
using Sitecore.Collections;
using Sitecore.Data.Items;
using Sitecore.DependencyInjection;

using Foundation.Kernel.Pipelines.DataViewChildItems;
using Foundation.Kernel.Models.DataViews;

namespace Foundation.Kernel.DataViews
{
	public class PipelineDrivenDataView : BucketDataView
	{
		protected override void GetChildItems(ItemCollection children, Item parent)
		{
			base.GetChildItems(children, parent);
			DataViewChildItemsArgs args = CreateDataViewChildItemsArgs(children, parent);
			if(args == null)
			{
				return;
			}

			ICorePipeline corePipeline = GetService<ICorePipeline>();
			PipelineDrivenDataViewSettings settings = GetSettings();

			if (corePipeline == null || string.IsNullOrWhiteSpace(settings?.PipelineDomain) || string.IsNullOrWhiteSpace(settings?.PipelineName))
			{
				return;
			}

			corePipeline.Run(settings.PipelineName, args, settings.PipelineDomain);
		}

		protected virtual ICorePipeline GetCorePipeline() => GetService<ICorePipeline>();

		protected virtual PipelineDrivenDataViewSettings GetSettings() => GetService<PipelineDrivenDataViewSettings>();

		protected virtual TService GetService<TService>() where TService : class => ServiceLocator.ServiceProvider.GetService(typeof(TService)) as TService;

		protected virtual DataViewChildItemsArgs CreateDataViewChildItemsArgs(ItemCollection children, Item parent) => new DataViewChildItemsArgs(children, parent);
	}
}

The MasterDataView above creates a new DataViewChildItemsArgs instance by passing the Children ItemCollection and parent; the custom pipeline will ultimately modify just the Children collection but I built this being mindful that other pipeline processors might need to change the Parent Item for their purposes; I just don’t have this as an example on this post.

This DataViewChildItemsArgs instance is then passed to a call to the custom pipeline within the defined domain.

I then defined the new pipeline along with the custom MasterDataView, both defined above, in the Sitecore configuration patch file below:

<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>
		<dataviews>
			<dataview name="Master">
				<patch:attribute name="assembly">Foundation.Kernel</patch:attribute>
				<patch:attribute name="type">Foundation.Kernel.DataViews.PipelineDrivenDataView</patch:attribute>
			</dataview>
		</dataviews>
		<moduleSettings>
			<foundation>
				<kernel>
					<pipelineDrivenDataViewSettings type="Foundation.Kernel.Models.DataViews.PipelineDrivenDataViewSettings, Foundation.Kernel" singleInstance="true">
						<PipelineDomain>masterDataView</PipelineDomain>
						<PipelineName>dataviewChildItems</PipelineName >
					</pipelineDrivenDataViewSettings>
				</kernel>
			</foundation>
		</moduleSettings>
		<pipelines>
			<group groupName="masterDataView" name="masterDataView">
				<pipelines>
					<dataviewChildItems>
					</dataviewChildItems>
				</pipelines>
			</group>
		</pipelines>
	</sitecore>
</configuration>

I wanted to abstract out the logic which determines if an Item has almost or too many child items as the project I was working on had more than one customization of the content editor to let content authors know it’s time to create a new folder for their items — dont worry I’ll blog about some of these in the future πŸ˜‰ — so I defined the following interface for turning these individual bits of functionality on and off:

using Foundation.Kernel.Services.FeatureToggle;

namespace Foundation.Validation.Services.TooManySubItems
{
	public interface ITooManySubItemsFeatureToggleService
	{
		bool IsEnabled(IFeatureToggleable toggleable);
	}
}

Here’s the implementation of the IFeatureToggleable interface above — this is a service Config Object which will be the global settings object to turn on and off the entire feature set of these Content Editor customizations — and will be injected into the implementation of the ITooManySubItemsFeatureToggleService further below:

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

using Foundation.Kernel.Services.FeatureToggle;

namespace Foundation.Validation.Models.TooManySubItems
{
	[ServiceConfigObject(ConfigPath = "moduleSettings/foundation/validation/tooManySubItemsSettings ", Lifetime = Lifetime.Singleton)]
	public class TooManySubItemsSettings : IFeatureToggleable
	{
		public bool Enabled { get; set; }

		public int NumberOfItemsToStartWarningUser{ get; set; }

		public int MaximumNumberOfItemsInFolder { get; set; }
	}
}

The Config Object above also has values on the number of child items on when to start warning the content authors they have almost too many child items, and also a value on when there are too many child items.

I then defined the TooManySubItemsSettings Config Object in the Sitecore patch configuration file below:

<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>
		<moduleSettings>
			<foundation>
				<validation>
					<tooManySubItemsSettings type="Foundation.Validation.Models.TooManySubItems.TooManySubItemsSettings, Foundation.Validation" singleInstance="true">
						<Enabled>true</Enabled>
						<!-- <MaximumNumberOfItemsInFolder> must be set and greater than zero in order for the feature to work.  In other words, default Sitecore functionality will execute 
							when this isn't set (it's essentially like setting <Enabled>false</Enabled> above; you can't warn users they are approaching a maximum number of items in a folder
							when there is no maximum value set.
						-->
						<NumberOfItemsToStartWarningUser>2</NumberOfItemsToStartWarningUser>
						<MaximumNumberOfItemsInFolder>4</MaximumNumberOfItemsInFolder>
					</tooManySubItemsSettings>
				</validation>
			</foundation>
		</moduleSettings>
	</sitecore>
</configuration>

For testing, I set the values on when to warning content authors to a low number for testing but ideally, MaximumNumberOfItemsInFolder should be 100 child Items, and I chose 95 child Items for NumberOfItemsToStartWarningUser in the production version of this.

Now, we need an implementation of the ITooManySubItemsFeatureToggleService interface above. The following is just that:

using Foundation.Kernel.Services.FeatureToggle;

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

namespace Feature.Validation.Services.TooManySubItems
{
	public class TooManySubItemsFeatureToggleService : ITooManySubItemsFeatureToggleService
	{
		private readonly TooManySubItemsSettings _settings;
		private readonly IFeatureToggleService _featureToggleService;

		public TooManySubItemsFeatureToggleService(TooManySubItemsSettings settings, IFeatureToggleService featureToggleService)
		{
			_settings = settings;
			_featureToggleService = featureToggleService;
		}

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

The class above will delegate to the IFeatureToggleService instance to determine if the entire feature set of visual cues to the content authors should be turned on/off, or if an invidual piece of functionality should be turned on/off; each IFeatureToggleable will also pass itself to this service so that it can determine whether it should be turned on/off.

Most of the features I had built for this will warn content authors for when they are approaching too many child Items and also convey there are too many child items but there were a few where I only had to let content authors know when they had reached the maximum number of child Items under an item. Due to this, I broke all of these values out into the following interfaces:

namespace Foundation.Validation.Services.TooManySubItems
{
	public interface ITooManySubItemsMaxItemsInFolderFeature
	{
		int MaximumNumberOfItemsInFolder { get; set; }
	}
}

The above interface is only for features which will let the content authors know when they are reached too many child Items.

namespace Foundation.Validation.Services.TooManySubItems
{
	public interface ITooManySubItemsWarnUserFeature
	{
		int NumberOfItemsToStartWarningUser { get; set; }
	}
}

The ITooManySubItemsWarnUserFeature is only for features that will warn users that they approaching too many child Items.

using System.Collections.Generic;

namespace Foundation.Validation.Services.TooManySubItems
{
	public interface ITooManySubItemsFeature : ITooManySubItemsMaxItemsInFolderFeature, ITooManySubItemsWarnUserFeature
	{
		List<string> TemplateIdsToIgnore { get; set; }
	}
}

The above interface is an amalgam of the two interfaces above, and also includes a collection of Template IDs to ignore during the almost or has too many child Items check — I had included this in case there are reasons for ignoring certain child Items with certain templates but am not using this in this blog post.

Due to the need of passing a bunch of stuff to the service which determines that an Item has almost or too many child Items, I created the following parameters object class:

using System.Collections.Generic;

using Sitecore.Data.Items;

namespace Foundation.Validation.Models.TooManySubItems
{
	public class TooManySubItemsServiceParameters
	{
		public Item Item { get; set; }

		public int NumberOfItemsToStartWarningUser { get; set; }

		public int MaximumNumberOfItemsInFolder { get; set; }

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

I then created a factory object to create TooManySubItemsServiceParameters instances above. The following is an interface for this service:

using System.Collections.Generic;

using Sitecore.Data.Items;

using Foundation.Validation.Models.TooManySubItems;

namespace Foundation.Validation.Services.TooManySubItems.Factories
{
	public interface ITooManySubItemsServiceParametersFactory
	{
		TooManySubItemsServiceParameters CreateParameters(TooManySubItemsCommandServiceParameters parameters); // not used in this post; I will discuss in a future post
		
		TooManySubItemsServiceParameters CreateParameters(Item item, ITooManySubItemsFeature feature);

		TooManySubItemsServiceParameters CreateParameters(Item item, int numberOfItemsToStartWarningUser, int maximumNumberOfItemsInFolder, List<string> templateIdsToIgnore);
	}
}

Here’s the implementation of the interface above:

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

using Sitecore.Data.Items;

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

namespace Feature.Validation.Services.TooManySubItems.Factories
{
	public class TooManySubItemsServiceParametersFactory : ITooManySubItemsServiceParametersFactory
	{
		// not used in this post; I will discuss in a future post
		public TooManySubItemsServiceParameters CreateParameters(TooManySubItemsCommandServiceParameters parameters) => CreateParameters(GetItem(parameters), 0, parameters.Feature.MaximumNumberOfItemsInFolder, parameters.TemplateIdsToIgnore);

		protected virtual Item GetItem(TooManySubItemsCommandServiceParameters parameters) => parameters?.Context.Items.FirstOrDefault();

		public TooManySubItemsServiceParameters CreateParameters(Item item, ITooManySubItemsFeature feature) => CreateParameters(item, feature.NumberOfItemsToStartWarningUser, feature.MaximumNumberOfItemsInFolder, feature.TemplateIdsToIgnore);

		public TooManySubItemsServiceParameters CreateParameters(Item item, int numberOfItemsToStartWarningUser, int maximumNumberOfItemsInFolder, List<string> templateIdsToIgnore)
		{ 
			return new TooManySubItemsServiceParameters
			{
				Item = item,
				NumberOfItemsToStartWarningUser = numberOfItemsToStartWarningUser,
				MaximumNumberOfItemsInFolder = maximumNumberOfItemsInFolder,
				TemplateIdsToIgnore = templateIdsToIgnore
			};
		}
	}
}

It’s basically just creating TooManySubItemsServiceParameters instances based on values passed.

We are ready to define the service which determines if an Item has almost or too many child Items. The following interface is for that service:

using Foundation.Validation.Models.TooManySubItems;

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

		bool HasTooManySubItems(TooManySubItemsServiceParameters parameters);
	}
}

The following is the implementation of the interface defined above:

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;
		}

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

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

			return _settings.NumberOfItemsToStartWarningUser;
		}

		protected virtual 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 HasAlmostTooManySubItems() method above determines if the passed Item has almost too many child items but not greater than or equal to the maximum number of child items as that check is governed by the HasTooManySubItems() method — it determines if the number of child items equals or exceeds the maximum number of child Items passed via the Parameters object; both of these methods only look at Items who do not have Template IDs which are in the TemplateIdsToIgnore list though we are not using this on this blog post.

Since we are modifying the appear of Item names in the Content Tree, I wanted to put markup for this in a nice place. I decided to use Handlebars.Net for this solution — in an earlier iteration of this project, I had this markup in a Sitecore configuration file all HTML encoded but it seemed like a cumbersome solution especially if the markup had to be changed so I went with this alternative approach instead as I had a good experience using Handlebars.Net over 2 years ago — but to my dismay, the current version of Handlebars.Net did not come with its OOTB ViewEngineFileSystem any longer as older versions had, so I had to create one myself. This required me to create the following service class for dealing with files but I’m ultimately just delegating to methods on Sitecore.IO.FileUtil (this lives in Sitecore.Kernel.dll) through this service:

namespace Foundation.Kernel.Services.IO
{
	public interface IFileUtilService
	{
		bool Exists(string path);

		string MakePath(string part1, string part2);

		string ReadFromFile(string path);
	}
}

The following implements the interface above but just delegates to methods on Sitecore.IO.FileUtil:

using Sitecore.IO;

namespace Foundation.Kernel.Services.IO
{
	public class FileUtilService : IFileUtilService
	{
		public bool Exists(string path) => FileUtil.Exists(path);

		public string MakePath(string part1, string part2) => FileUtil.MakePath(part1, part2);

		public string ReadFromFile(string path) => FileUtil.ReadFromFile(path);
	}
}

I then created a subclass of HandlebarsDotNet.ViewEngineFileSystem; this adapter class talks to the file system for HandlebarsDotNet:

using HandlebarsDotNet;

using Foundation.Kernel.Services.IO;

namespace Feature.Token.Services
{
	public class HandlebarsViewEngineFileSystem : ViewEngineFileSystem
	{
		private readonly IFileUtilService _fileUtilService;

		public HandlebarsViewEngineFileSystem(IFileUtilService fileUtilService)
		{
			_fileUtilService = fileUtilService;
		}

		public override bool FileExists(string filePath) => _fileUtilService.Exists(filePath);

		public override string GetFileContent(string filename) => _fileUtilService.ReadFromFile(filename);

		protected override string CombinePath(string dir, string otherFileName) => _fileUtilService.MakePath(dir, otherFileName);
	}
}

I then wrapped HandlebarsDotNet’s main static class in a service class so I can inject things together using DI. The following interface is for this service class:

using System;
using System.IO;

using HandlebarsDotNet;

namespace Feature.Token.Services
{
	public interface IHandlebarsService
	{
		Action<TextWriter, object> Compile(TextReader template);

		Func<object, string> Compile(string template);

		Func<object, string> CompileView(string templatePath);

		void RegisterHelper(string helperName, HandlebarsHelper helperFunction);

		void RegisterHelper(string helperName, HandlebarsBlockHelper helperFunction);

		void RegisterTemplate(string templateName, Action<TextWriter, object> template);

		void RegisterTemplate(string templateName, string template);
	}
}

We now need to implement the interface above:

using System;
using System.IO;

using HandlebarsDotNet;

namespace Feature.Token.Services
{
	public class HandlebarsService : IHandlebarsService
	{
		private ViewEngineFileSystem _viewEngineFileSystem;

		public HandlebarsService(ViewEngineFileSystem viewEngineFileSystem)
		{
			_viewEngineFileSystem = viewEngineFileSystem;
		}

		private readonly object _locker = new object();
		private IHandlebars _instance;
		private IHandlebars Instance
		{
			get
			{
				if(_instance  == null)
				{
					lock(_locker)
					{
						_instance = Create();
					}
				}

				return _instance;
			}
		}

		private IHandlebars Create(HandlebarsConfiguration configuration = null)
		{
			IHandlebars handlebars = Handlebars.Create(configuration);
			handlebars.Configuration.FileSystem = _viewEngineFileSystem;
			return handlebars;
		}

		public Action<TextWriter, object> Compile(TextReader template) => Instance.Compile(template);

		public Func<object, string> Compile(string template) => Instance.Compile(template);

		public Func<object, string> CompileView(string templatePath) => Instance.CompileView(templatePath);

		public void RegisterHelper(string helperName, HandlebarsHelper helperFunction) => Instance.RegisterHelper(helperName, helperFunction);

		public void RegisterHelper(string helperName, HandlebarsBlockHelper helperFunction) => Instance.RegisterHelper(helperName, helperFunction);

		public void RegisterTemplate(string templateName, Action<TextWriter, object> template) => Instance.RegisterTemplate(templateName, template);

		public void RegisterTemplate(string templateName, string template) => Instance.RegisterTemplate(templateName, template);
	}
}

I’m injecting the ViewEngineFileSystem service — this is an instance of the HandlebarsViewEngineFileSystem above but I stick it into the IoC contain with a service type of ViewEngineFileSystem — into this class so we can create a new IHandlebars instance using Handlebars’s Create() method, and then it gets stored in an internal Singleton in the class.

Next, we need a service to replace tokens (no, not Sitecore tokens such as $name, $id, etc) but tokens in handlebars template strings or files:

using System.Collections.Generic;

namespace Foundation.Token.Services
{
	public interface ITemplatedTokenReplacer
	{
		string ReplaceTokens(string input, IDictionary<string, string> tokens);

		string ReplaceTokens(string templateSource, object tokens);

		string ReplaceTokensFromView(string viewPath, IDictionary<string, string> tokens);

		string ReplaceTokensFromView(string viewPath, object tokens);

		void ClearAllCaches();

		void ClearTemplateCache();

		void ClearViewsCache();
	}
}

The implementation of the interface above will need to “cache” compiled Handlebars.Net templates as the documentation calls out that compiling their templates is an expensive operation and recommend caching these compilations so I am using the following interface to create services which cache things:

namespace Foundation.Kernel.Services.Cache
{
    public interface ICacher<TKey, TObject>
    {
        void Add(TKey key, TObject obj);

        bool ContainsKey(TKey key);

        TObject Get(TKey key);

        void Clear();
    }
}

Classes which implement this base class below will be caching their things in a ConcurrentDictionary:

using System.Collections.Concurrent;
using System.Collections.Generic;

namespace Foundation.Kernel.Services.Cache
{
    public abstract class Cacher<TKey, TObject>
    {
        private IDictionary<TKey, TObject> _cache;

        protected Cacher()
        {
            _cache = CreateCache();
        }

        protected virtual IDictionary<TKey, TObject> CreateCache() => new ConcurrentDictionary<TKey, TObject>();

        public void Add(TKey key, TObject obj) => _cache[key] = obj;

        public TObject Get(TKey key) => ContainsKey(key) ? _cache[key] : default(TObject);

        public bool ContainsKey(TKey key) => _cache.ContainsKey(key);

        public void Clear() => _cache.Clear();
    }
}

We will be caching compilations of Handlebars.Net template strings:

using System;

using Foundation.Kernel.Services.Cache;

namespace Feature.Token.Services.Cachers
{
	public interface ICompiledTemplateCache : ICacher<string, Func<object, string>>
	{
	}
}
using System;

using Foundation.Kernel.Services.Cache;

namespace Feature.Token.Services.Cachers
{
	public class CompiledTemplateCache : Cacher<string, Func<object, string>>, ICompiledTemplateCache
	{
	}
}

We will also be caching compilations of Handlebars.Net template strings sourced from files:

using System;

using Foundation.Kernel.Services.Cache;

namespace Feature.Token.Services.Cachers
{
	public interface ICompiledViewTemplateCache : ICacher<string, Func<object, string>>
	{
	}
}
using System;

using Foundation.Kernel.Services.Cache;

namespace Feature.Token.Services.Cachers
{
	public class CompiledViewTemplateCache : Cacher<string, Func<object, string>>, ICompiledViewTemplateCache
	{

	}
}

Finally, we can talk about the implemenation of ITemplatedTokenReplacer πŸ˜‰ :

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;

using HandlebarsDotNet;

using Foundation.Token.Services;

using Feature.Token.Services.Cachers;

namespace Feature.Token.Services
{
	public class HandlebarsTokenReplacer : ITemplatedTokenReplacer
	{
		private readonly IHandlebarsService _handlebarsService;

		private readonly ICompiledTemplateCache _compiledTemplateCache;
		private readonly ICompiledViewTemplateCache _compiledViewTemplateCache;

		public HandlebarsTokenReplacer(IHandlebarsService handlebarsService, ICompiledTemplateCache compiledTemplateCache, ICompiledViewTemplateCache compiledViewTemplateCache)
		{
			_handlebarsService = handlebarsService;
			_compiledTemplateCache = compiledTemplateCache;
			_compiledViewTemplateCache = compiledViewTemplateCache;
		}

		public string ReplaceTokens(string input, IDictionary<string, string> tokens) => ReplaceTokens(input, (object)tokens);

		public string ReplaceTokens(string templateSource, object tokens)
		{
			var compiled = GetCompiledTemplateFromCache(templateSource);
			if (compiled == null)
			{
				compiled = Compile(templateSource);
				AddCompiledTemplateToCache(templateSource, compiled);
			}

			return ReplaceTokens(compiled, tokens);
		}

		protected virtual Func<object, string> GetCompiledTemplateFromCache(string templateSource) => _compiledTemplateCache.Get(templateSource);

		protected virtual Func<object, string> Compile(string viewPath) => _handlebarsService.Compile(viewPath);

		protected virtual void AddCompiledTemplateToCache(string templateSource, Func<object, string> compiled) => _compiledTemplateCache.Add(templateSource, compiled);

		protected virtual string ReplaceTokens(Func<object, string> template, object tokens)
		{
			if(template == null)
			{
				return string.Empty;
			}

			return template(tokens);
		}

		public string ReplaceTokensFromView(string viewPath, IDictionary<string, string> tokens) => ReplaceTokensFromView(viewPath, (object)tokens);

		public string ReplaceTokensFromView(string viewPath, object tokens)
		{
			var compiled = GetCompiledViewTemplateFromCache(viewPath);
			if(compiled == null)
			{
				compiled = CompileView(viewPath);
				AddCompiledViewTemplateToCache(viewPath, compiled);
			}

			return ReplaceTokens(compiled, tokens);
		}

		protected virtual Func<object, string> GetCompiledViewTemplateFromCache(string viewPath) => _compiledViewTemplateCache.Get(viewPath);

		protected virtual Func<object, string> CompileView(string viewPath) => _handlebarsService.CompileView(viewPath);

		protected virtual void AddCompiledViewTemplateToCache(string viewPath, Func<object, string> compiled) => _compiledViewTemplateCache.Add(viewPath, compiled);

		protected virtual Action<TextWriter, object> Compile(TextReader template) => _handlebarsService.Compile(template);

		protected virtual void RegisterHelper(string helperName, HandlebarsHelper helperFunction) => _handlebarsService.RegisterHelper(helperName, helperFunction);

		protected virtual void RegisterHelper(string helperName, HandlebarsBlockHelper helperFunction) => _handlebarsService.RegisterHelper(helperName, helperFunction);

		protected virtual void RegisterTemplate(string templateName, Action<TextWriter, object> template) => _handlebarsService.RegisterTemplate(templateName, template);

		protected virtual void RegisterTemplate(string templateName, string template) => _handlebarsService.RegisterTemplate(templateName, template);

		public void ClearAllCaches()
		{
			ClearTemplateCache();
			ClearViewsCache();
		}

		public void ClearTemplateCache() => _compiledTemplateCache.Clear();

		public void ClearViewsCache() => _compiledViewTemplateCache.Clear();
	}
}

Ultimately, the class above delegates calls to IHandlebarsService for the most part. Lookups are done to replace tokens in templates before compiling to see if these compilations were cached. If there were not cached, they are compiled and then cached. The resulting token replacement strings are returned for both template string replacements, and those sourced from template files.

I also have methods to clear the two caches.

Next, I created a custom FileWatcher to clear the Compile Views cache of Handlebars.Net template file compliations. Since FileWatchers are defined in the Web.config, I decided to create a service class which will control the logic which my cusotm FileWatcher will completely delegate its functionality to. The following interface is for that service class:

namespace Feature.Token.Services
{
	public interface IHandlebarsViewFilesWatcherService
	{
		void Created(string fullPath);

		void Deleted(string filePath);

		void Renamed(string filePath, string oldFilePath);
	}
}

The service class will take in a service Config Object defined in a Sitecore configuration file further below:

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

namespace Feature.Token.Models.FileWatchers
{
	[ServiceConfigObject(ConfigPath = "moduleSettings/feature/token/handlebarsViewFilesWatcherServiceSettings", Lifetime = Lifetime.Singleton)]
	public class HandlebarsViewFilesWatcherServiceSettings
	{
		public string CreatedLogInfoMessageFormat { get; set; }

		public string DeletedLogInfoMessageFormat { get; set; }

		public string RenamedLogInfoMessageFormat { get; set; }

		public string ClearViewsCacheLogInfoMessage { get; set; }
	}
}

Here is the implemenation of the interface above:

using Sitecore.Abstractions;

using Foundation.Token.Services;

using Feature.Token.Models.FileWatchers;

namespace Feature.Token.Services
{
	public class HandlebarsViewFilesWatcherService : IHandlebarsViewFilesWatcherService
	{
		private readonly HandlebarsViewFilesWatcherServiceSettings _settings;
		private readonly ITemplatedTokenReplacer _replacer;
		private readonly BaseLog _log;

		public HandlebarsViewFilesWatcherService(HandlebarsViewFilesWatcherServiceSettings settings, ITemplatedTokenReplacer replacer, BaseLog log)
		{
			_settings = settings;
			_replacer = replacer;
			_log = log;
		}

		public void Created(string fullPath)
		{
			LogInfo(string.Format(_settings.CreatedLogInfoMessageFormat, fullPath), this);
			ClearViewsCacheLogInfo();
		}

		public void Deleted(string filePath)
		{
			LogInfo(string.Format(_settings.DeletedLogInfoMessageFormat, filePath), this);
			ClearViewsCacheLogInfo();
		}

		public void Renamed(string filePath, string oldFilePath)
		{
			LogInfo(string.Format(_settings.RenamedLogInfoMessageFormat, filePath, oldFilePath), this);
			ClearViewsCacheLogInfo();
		}

		protected virtual void LogInfo(string message, object owner) => _log.Info(message, owner);

		protected virtual void ClearViewsCacheLogInfo()
		{
			ClearViewsCache();
			LogInfo(_settings.ClearViewsCacheLogInfoMessage, this);
		}

		protected virtual void ClearViewsCache() => _replacer.ClearViewsCache();
	}
}

If a Handlebars.Net template file is modified, added, renamed or deleted, the Handlebars.Net Views Cache is cleared so that changes will be reflected in the Content Tree immediately.

I then created the “real” FileWatcher class; this is the class which will be registered in my Web.config to make this work:

using Sitecore.DependencyInjection;
using Sitecore.IO;

using Feature.Token.Services;

namespace Feature.Token.FileWatchers
{
	public class HandlebarsViewFilesWatcher : FileWatcher
	{
		public HandlebarsViewFilesWatcher()
			: base("watchers/handlebars")
		{
		}

		protected override void Created(string fullPath) => GetService<IHandlebarsViewFilesWatcherService>().Created(fullPath);

		protected override void Deleted(string filePath) => GetService<IHandlebarsViewFilesWatcherService>().Deleted(filePath);

		protected override void Renamed(string filePath, string oldFilePath) => GetService<IHandlebarsViewFilesWatcherService>().Renamed(filePath, oldFilePath);

		protected TService GetService<TService>() where TService : class => ServiceLocator.ServiceProvider.GetService(typeof(TService)) as TService;
	}
}

With that in place, I defined it in my Web.config here:


  <!-- More stuff here -->
  
 <system.webServer>
    <modules runAllManagedModulesForAllRequests="true">
	
	  <!-- More stuff here -->
	  
	  <!-- Handlerbars view files watcher -->
	  <add type="Feature.Token.FileWatchers.HandlebarsViewFilesWatcher, Feature.Token" name="HandlebarsViewFilesWatcher"/>  

	    <!-- More stuff here -->
	</modules>
</system.webServer>

<!-- More stuff here -->

Now, we need a custom Sitecore patch configuration file to wire-up all of the services defined above for token replacement along with the settings object for the FileWatcher service:

<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>
	  <moduleSettings>
		  <feature>
			  <token>
				  <handlebarsViewFilesWatcherServiceSettings type="Feature.Token.Models.FileWatchers.HandlebarsViewFilesWatcherServiceSettings, Feature.Token" singleInstance="true">
					  <CreatedLogInfoMessageFormat>Handlebars view file created or modified: {0}.</CreatedLogInfoMessageFormat>
					  <DeletedLogInfoMessageFormat>Handlebars view file deleted: {0}.</DeletedLogInfoMessageFormat>
					  <RenamedLogInfoMessageFormat>Handlebars view file renamed: {0}. Old path: {1}</RenamedLogInfoMessageFormat>
					  <ClearViewsCacheLogInfoMessage>Token Views Cache Cleared.</ClearViewsCacheLogInfoMessage>
				  </handlebarsViewFilesWatcherServiceSettings>
			  </token>	  
		  </feature>
	  </moduleSettings>	  
	  <services>
		  <register serviceType="Feature.Token.Services.IHandlebarsService, Feature.Token"
					implementationType="Feature.Token.Services.HandlebarsService, Feature.Token"
					lifetime="Singleton" />
		  <register serviceType="Foundation.Token.Services.ITemplatedTokenReplacer, Foundation.Token"
					implementationType="Feature.Token.Services.HandlebarsTokenReplacer, Feature.Token"
					lifetime="Singleton" />
		  <register serviceType="HandlebarsDotNet.ViewEngineFileSystem, Handlebars"
					implementationType="Feature.Token.Services.HandlebarsViewEngineFileSystem, Feature.Token"
					lifetime="Singleton" />
		  <register serviceType="Feature.Token.Services.IHandlebarsViewFilesWatcherService, Feature.Token"
					implementationType="Feature.Token.Services.HandlebarsViewFilesWatcherService, Feature.Token"
					lifetime="Singleton" />
		  <register serviceType="Feature.Token.Services.Cachers.ICompiledTemplateCache, Feature.Token"
					implementationType="Feature.Token.Services.Cachers.CompiledTemplateCache, Feature.Token"
					lifetime="Singleton" />
		  <register serviceType="Feature.Token.Services.Cachers.ICompiledViewTemplateCache, Feature.Token"
					implementationType="Feature.Token.Services.Cachers.CompiledViewTemplateCache, Feature.Token"
					lifetime="Singleton" />
	  </services>
	  <watchers>
		  <handlebars>
			  <folder>/Views</folder>
			  <filter>*.hbs</filter>
		  </handlebars>
	  </watchers>
  </sitecore>
</configuration>

Are you still with me? πŸ˜‰

Now, let’s define some other service classes which just wrap static method calls on things in the Sitecore Api; I wanted to source these all from the Sitecore IoC container, and not all things in the Sitecore Api are in the IoC container so I’ll just stick them in there. I’m not going to explain too much about these except that they are used in subsequent classes further down:

This interface with its implemenation wraps calls on the Sitecore.Data.ID class:

using System;

using Sitecore.Data;

namespace Foundation.Kernel.Services.UniqueIdentifier
{
    public interface IIDService
    {
        bool IsNullOrEmpty(ID id);

        bool TryParse(string value, out ID id);

        bool TryParse(object value, out ID id);

        ID Parse(string value);

        ID Parse(object value);

        ID CreateNewID();

        ID CreateNewID(string id); // not used in this post

        ID CreateNewID(Guid id);  // not used in this post
    }
}
using System;

using Sitecore.Data;

using Foundation.Kernel.Services.UniqueIdentifier.Factories;

namespace Foundation.Kernel.Services.UniqueIdentifier
{
    public class IDService : IIDService
    {
        private readonly IIDFactory _idFactory;   // not used in this post

        public IDService(IIDFactory idFactory)
        {
            _idFactory = idFactory;   // not used in this post
        }

        public bool IsNullOrEmpty(ID id) => ID.IsNullOrEmpty(id);

        public bool TryParse(string value, out ID id) => ID.TryParse(value, out id);

        public bool TryParse(object value, out ID id) => ID.TryParse(value, out id);

        public ID Parse(string value) => ID.Parse(value);

        public ID Parse(object value) => ID.Parse(value);

        public ID CreateNewID() => _idFactory.CreateNewID();

        public ID CreateNewID(string id) => _idFactory.CreateNewID(id);   // not used in this post

        public ID CreateNewID(Guid id) => _idFactory.CreateNewID(id);   // not used in this post
    }
}

This interface with its implemenation creates a FieldList instance from a FieldCollection instance along with pointers to methods for changing field values being placed from the FieldCollection into a new FieldList instance (this will be used further down in this post when modifying how Items appear in the content tree):

using System;

using Sitecore.Collections;
using Sitecore.Data;
using Sitecore.Data.Fields;

namespace Foundation.Kernel.Services.Fields.Factories
{
	public interface IFieldsFactory
	{
		FieldList CreateNewFieldList(FieldCollection fieldCollection, Func<Field, ID> idProvider, Func<Field, string> valueProvider);

		FieldList CreateNewFieldList();
	}
}
using System;

using Sitecore.Collections;
using Sitecore.Data;
using Sitecore.Data.Fields;

namespace Foundation.Kernel.Services.Fields.Factories
{
	public class FieldsFactory : IFieldsFactory
	{
		public FieldList CreateNewFieldList(FieldCollection fieldCollection, Func<Field, ID> idProvider, Func<Field, string> valueProvider)
		{
			FieldList fieldList = CreateNewFieldList();
			foreach (Field field in fieldCollection)
			{
				ID id = idProvider(field);
				string value = valueProvider(field);
				fieldList.Add(id, value);
			}

			return fieldList;
		}

		public FieldList CreateNewFieldList() => new FieldList();
	}
}

This interface and its implementation wrap calls to Sitecore.Globalization.Translate:

namespace Foundation.Kernel.Services.Globalization
{
	public interface ITranslateService
	{
		string TranslateText(string key);
	}
}
namespace Foundation.Kernel.Services.Globalization
{
	public class TranslateService : ITranslateService
	{
		public string TranslateText(string key) => Sitecore.Globalization.Translate.Text(key);
	}
}

This interface and its implementation wrap calls to Sitecore.Resources.Images; this will be used when dealing with Icon paths:

using Sitecore.Web.UI;

namespace Foundation.Kernel.Services.Media.Resources
{
	public interface IImagesService
	{
		string GetThemedImageSource(string image);

		string GetThemedImageSource(string image, ImageDimension dimension);
	}
}
using Sitecore.Resources;
using Sitecore.Web.UI;

namespace Foundation.Kernel.Services.Media.Resources
{
	public class ImagesService : IImagesService
	{
		public string GetThemedImageSource(string image) => Images.GetThemedImageSource(image);

		public string GetThemedImageSource(string image, ImageDimension dimension) => Images.GetThemedImageSource(image, dimension);
	}
}

This interface and its implementation will create instances of the Item class but with modified values sourced from another Item instance — this is used when changing the Item name for display in the content tree, and these items are “temporary”:

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

namespace Foundation.Kernel.Services.Items.Factories
{
	public interface IItemFactory
	{
		Item CreateAlteredItem(Item item, string itemName, string iconPath, ID templateId, ID branchId, Language language, Version version);

		Item CreateNewItem(ID itemID, ItemData data, Database database, bool isTemporary);
		
		ItemDefinition CreateItemDefinition(ID itemID, string itemName, ID templateID, ID branchId);

		ItemData CreateItemData(ItemDefinition definition, Language language, Version version, FieldList fields);
	}
}
using System;

using Sitecore;
using Sitecore.Collections;
using Sitecore.Data;
using Sitecore.Data.Fields;
using Sitecore.Data.Items;
using Sitecore.Globalization;

using Foundation.Kernel.Services.Globalization;
using Foundation.Kernel.Services.Media.Resources;
using Foundation.Kernel.Services.UniqueIdentifier;
using Foundation.Kernel.Services.Fields.Factories;

namespace Foundation.Kernel.Services.Items.Factories
{
	public class ItemFactory : IItemFactory
	{
		private readonly IIDService _idService;
		private readonly IFieldsFactory _fieldsFactory;
		private readonly ITranslateService _translateService;
		private readonly IImagesService _imagesService;

		public ItemFactory(IIDService idService, IFieldsFactory fieldsFactory, ITranslateService translateService, IImagesService imagesService)
		{
			_idService = idService;
			_fieldsFactory = fieldsFactory;
			_translateService = translateService;
			_imagesService = imagesService;
		}

		public Item CreateAlteredItem(Item item, string itemName, string iconPath, ID templateId, ID branchId, Language language, Sitecore.Data.Version version)
		{
			if (item == null || IsNullOrEmpty(templateId))
			{
				return null;
			}

			ItemDefinition definition = CreateItemDefinition(item.ID, itemName, templateId, branchId);
			if (definition == null)
			{
				return null;
			}

			FieldList fields = CreateNewFieldList(item?.Fields, field => field.ID, field => GetAlteredItemFieldValue(field, itemName, iconPath));
			if (fields == null)
			{
				return null;
			}

			return CreateNewItem(item.ID, CreateItemData(definition, language, version, fields), item.Database, true);
		}

		protected virtual bool IsNullOrEmpty(ID id) => _idService.IsNullOrEmpty(id);

		protected virtual string GetAlteredItemFieldValue(Field field, string itemName, string iconPath)
		{
			string value = field.Value;
			if (field.ID == GetDisplayNameFieldId() && !string.IsNullOrWhiteSpace(itemName))
			{
				value = TranslateText(itemName);
			}
			else if (field.ID == GetIconFieldId() && !string.IsNullOrWhiteSpace(iconPath))
			{
				value = GetThemedImageSource(iconPath);
			}

			return value;
		}

		protected virtual ID GetDisplayNameFieldId() => FieldIDs.DisplayName;

		protected virtual ID GetIconFieldId() => FieldIDs.Icon;

		protected virtual string TranslateText(string key) => _translateService.TranslateText(key);

		protected virtual string GetThemedImageSource(string image) => _imagesService.GetThemedImageSource(image);

		protected virtual FieldList CreateNewFieldList(FieldCollection fieldCollection, Func<Field, ID> idProvider, Func<Field, string> valueProvider) => _fieldsFactory.CreateNewFieldList(fieldCollection, idProvider, valueProvider);

		public Item CreateNewItem(ID itemID, ItemData data, Database database, bool isTemporary)
		{
			return new Item(itemID, data, database)
			{
				RuntimeSettings = { Temporary = isTemporary }
			};
		}

		public ItemDefinition CreateItemDefinition(ID itemID, string itemName, ID templateID, ID branchId) => new ItemDefinition(itemID, itemName, templateID, branchId);

		public ItemData CreateItemData(ItemDefinition definition, Language language, Sitecore.Data.Version version, FieldList fields) => new ItemData(definition, language, version, fields);
	}
}

This interface and its implementation wrap calls to the Sitecore.Data.Version class:

using Sitecore.Data;

namespace Foundation.Kernel.Services.Versoning
{
	public interface IVersionService
	{
		Version GetLatestVersion();
	}
}
using Sitecore.Data;

namespace Foundation.Kernel.Services.Versoning
{
	public class VersionService : IVersionService
	{
		public Version GetLatestVersion() => Version.Latest;
	}
}

This interface and its implementation wrap calls to the Sitecore.Globalization.Language class:

using Sitecore;
using Sitecore.Globalization;

namespace Foundation.Kernel.Services.Globalization
{
    public interface ILanguageService
    {
        bool TryParse(string name, out Language result);

        Language Parse(string name);

        Language GetContextLanguage();

		Language GetLanguageInvariant();
	}
}
using Sitecore;
using Sitecore.Globalization;

namespace Foundation.Kernel.Services.Globalization
{
    public class LanguageService : ILanguageService
    {
        public bool TryParse(string name, out Language result) => Language.TryParse(name, out result);

        public Language Parse(string name) => Language.Parse(name);

        public Language GetContextLanguage() => Context.Language;

		public Language GetLanguageInvariant() => Language.Invariant;
	}
}

Let’s tie everything together in the following implentation of the DataViewChildItemsProcessor class defined towards the top of this post:

using System;
using System.Collections.Generic;

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

using Foundation.Token.Services;

using Foundation.Kernel.Pipelines.DataViewChildItems;
using Foundation.Kernel.Services.FeatureToggle;
using Foundation.Kernel.Services.Items.Factories;
using Foundation.Kernel.Services.Versoning;
using Foundation.Kernel.Services.Globalization;

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

namespace Feature.ContentEditor.Pipelines.DataViewChildItems
{
	public class MediaItemTooManyChildItems : DataViewChildItemsProcessor, IMediaItemTooManyChildItems
	{
		private readonly ITooManySubItemsFeatureToggleService _tooManySubItemsFeatureToggleService;
		private readonly ITooManySubItemsServiceParametersFactory _tooManySubItemsServiceParametersFactory;
		private readonly ITooManySubItemsService _tooManySubItemsService;
		private readonly ITemplatedTokenReplacer _templatedTokenReplacer;
		private readonly IItemFactory _itemFactory;
		private readonly IVersionService _versionService;
		private readonly ILanguageService _languageService;

		private string MediaLibraryBasePath { get; set; }

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

		public int NumberOfItemsToStartWarningUser { get; set; }

		private string AlmostAtMaxiumIconPath { get; set; }

		private string AlmostAtMaxiumMessageViewPath { get; set; }

		public int MaximumNumberOfItemsInFolder { get; set; }

		private string AtMaxiumIconPath { get; set; }

		private string AtMaxiumMessageViewPath { get; set; }

		public MediaItemTooManyChildItems(IFeatureToggleService featureToggleService, ITooManySubItemsFeatureToggleService tooManySubItemsFeatureToggleService, ITooManySubItemsServiceParametersFactory tooManySubItemsServiceParametersFactory, ITooManySubItemsService tooManySubItemsService, ITemplatedTokenReplacer templatedTokenReplacer,  IItemFactory itemFactory, IVersionService versionService, ILanguageService languageService)
			: base(featureToggleService)
		{
			_tooManySubItemsFeatureToggleService = tooManySubItemsFeatureToggleService;
			_tooManySubItemsServiceParametersFactory = tooManySubItemsServiceParametersFactory;
			_tooManySubItemsService = tooManySubItemsService;
			_templatedTokenReplacer = templatedTokenReplacer;
			_itemFactory = itemFactory;
			_versionService = versionService;
			_languageService = languageService;
		}

		protected override bool ShouldExecute(DataViewChildItemsArgs args)
		{
			return IsMediaLibraryBasePathSet()
				&& HasRequiredParameters(args)
				&& IsInMediaLibrary(args?.Parent);
		}

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

		protected virtual bool IsMediaLibraryBasePathSet() => !string.IsNullOrWhiteSpace(GetMediaLibraryBasePath());

		protected virtual bool HasRequiredParameters(DataViewChildItemsArgs args) => args?.Parent != null && args?.Children != null;

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

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

		protected override void GetChildItems(DataViewChildItemsArgs args)
		{
			if (!HasRequiredParameters(args))
			{
				return;
			}

			for (int i = args.Children.Count - 1; i >= 0; i--)
			{
				Item child = args.Children[i];
				if(!IsInMediaLibrary(child) || IsMediaLibraryRootItem(child))
				{
					continue;
				}

				TooManySubItemsServiceParameters parameters = CreateParameters(child, this);
				bool hasAlmostTooManySubItems = HasAlmostTooManySubItems(parameters);
				bool hasTooManySubItems = HasTooManySubItems(parameters);
				if (!hasAlmostTooManySubItems && !hasTooManySubItems)
				{
					continue;
				}

				string itemName = string.Empty;
				string iconPath = string.Empty;
				if (hasAlmostTooManySubItems)
				{
					itemName = GetAlmostAtMaxiumMessage(child, GetAlmostAtMaxiumIconPath());
				}
				else if (hasTooManySubItems)
				{
					itemName = GetAtMaxiumMessage(child, GetAtMaxiumIconPath());
				}

				Item alteredItem = GetAlteredItem(child, itemName, iconPath, child.TemplateID);
				if(alteredItem == null)
				{
					return;
				}

				args.Children.RemoveAt(i);
				args.Children.Insert(i, alteredItem);
			}
		}

		protected virtual bool IsInMediaLibrary(Item item)
		{
			string mediaLibraryBasePath = GetMediaLibraryBasePath();
			if (item == null || string.IsNullOrWhiteSpace(mediaLibraryBasePath))
			{
				return false;
			}

			return item.Paths.FullPath.StartsWith(mediaLibraryBasePath, StringComparison.OrdinalIgnoreCase);
		}

		protected virtual bool IsMediaLibraryRootItem(Item item)
		{
			string mediaLibraryBasePath = GetMediaLibraryBasePath();
			if (item == null || string.IsNullOrWhiteSpace(mediaLibraryBasePath))
			{
				return false;
			}

			return item.Paths.FullPath.Equals(mediaLibraryBasePath, StringComparison.OrdinalIgnoreCase);
		}

		protected virtual string GetMediaLibraryBasePath() => MediaLibraryBasePath;

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

		protected virtual string GetAlmostAtMaxiumIconPath() => AlmostAtMaxiumIconPath;

		protected virtual string GetAtMaxiumIconPath() => AtMaxiumIconPath;

		protected virtual string GetAlmostAtMaxiumMessage(Item item, string iconPath) => ReplaceTokensFromView(GetAlmostAtMaxiumMessageViewPath(), new { ItemName = item.Name, IconPath = iconPath, ChildItemsCount = GetChildItemsCount(item) });

		protected virtual string GetAlmostAtMaxiumMessageViewPath() => AlmostAtMaxiumMessageViewPath;

		protected virtual string GetAtMaxiumMessage(Item item, string iconPath) => ReplaceTokensFromView(GetAtMaxiumMessageViewPath(), new { ItemName = item.Name, IconPath = iconPath, ChildItemsCount = GetChildItemsCount(item) });

		protected virtual string GetAtMaxiumMessageViewPath() => AtMaxiumMessageViewPath;

		protected virtual int GetChildItemsCount(Item item) => item.Children.Count;

		protected virtual string ReplaceTokensFromView(string viewPath, object tokens) => _templatedTokenReplacer.ReplaceTokensFromView(viewPath, tokens);

		protected virtual Item GetAlteredItem(Item item, string itemName, string iconPath, ID templateId) => CreateAlteredItem(item, itemName, iconPath, templateId, ID.Null, GetLanguageInvariant(), GetLatestVersion());

		protected virtual Item CreateAlteredItem(Item item, string itemName, string iconPath, ID templateId, ID branchId, Language language, Sitecore.Data.Version version) => _itemFactory.CreateAlteredItem(item, itemName, iconPath, templateId, branchId, language, version);

		protected virtual Language GetLanguageInvariant() => _languageService.GetLanguageInvariant();

		protected virtual Sitecore.Data.Version GetLatestVersion() => _versionService.GetLatestVersion();
	}
}

I’m not going to explain this entire class as there’s a lot of service delegation happening here but the gist is we will only create an “altered” item when the item in the Children collection on the DataViewChildItemsArgs instance is in the media library, and has almost, or does have too many child items. We then replace the child item in the DataViewChildItemsArgs Children collection with the “altered” item whose name includes the original item’s name and the icon associated with “almost has too many child items” or “has too many child items”; these are retrieved from their respective methods.

I then registered the pipeline processor above in the Sitecore configuration patch file below:

<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>
			<group name="masterDataView">
				<pipelines>
					<dataviewChildItems>
						<processor type="Feature.ContentEditor.Pipelines.DataViewChildItems.IMediaItemTooManyChildItems, Feature.ContentEditor" resolve="true">
							<Enabled>true</Enabled>
							<AbortPipelineAfterExecution>false</AbortPipelineAfterExecution>
							<MediaLibraryBasePath>/sitecore/media library/</MediaLibraryBasePath>
							<AlmostAtMaxiumIconPath>-/icon/Applicationsv2/16x16/sign_warning.png</AlmostAtMaxiumIconPath>
							<AtMaxiumIconPath>-/icon/Apps/16x16/Stop.png</AtMaxiumIconPath>
							<AlmostAtMaxiumMessageViewPath>/Views/DataViewChildItems/MediaItemTooManyChildItems/AlmostAtMaxiumMessageTemplate.hbs</AlmostAtMaxiumMessageViewPath>
							<AtMaxiumMessageViewPath>/Views/DataViewChildItems/MediaItemTooManyChildItems/AtMaxiumMessageTemplate.hbs</AtMaxiumMessageViewPath>
							<!-- By setting these, you can override the default values set for the entire feature set
										<NumberOfItemsToStartWarningUser>95</NumberOfItemsToStartWarningUser>
										<MaximumNumberOfItemsInFolder>100</MaximumNumberOfItemsInFolder>
							-->
						</processor>
					</dataviewChildItems>
				</pipelines>
			</group>
		</pipelines>
		<services>
			<register
				serviceType="Feature.ContentEditor.Pipelines.DataViewChildItems.IMediaItemTooManyChildItems, Feature.ContentEditor"
				implementationType="Feature.ContentEditor.Pipelines.DataViewChildItems.MediaItemTooManyChildItems, Feature.ContentEditor"
				lifetime="Singleton" />
		</services>
	</sitecore>
</configuration>

This is what’s inside of /Views/DataViewChildItems/MediaItemTooManyChildItems/AlmostAtMaxiumMessageTemplate.hbs; this a Handlebars.Net template file:

{{ItemName}}&nbsp;<img src="{{IconPath}}" />&nbsp;<span style="color: #999900;">({{ChildItemsCount}} child items)</span>

This is what’s inside of /Views/DataViewChildItems/MediaItemTooManyChildItems/AtMaxiumMessageTemplate.hbs; this a Handlebars.Net template file:

{{ItemName}}&nbsp;<img src="{{IconPath}}" />&nbsp;<span style="color: red;">({{ChildItemsCount}} child items)</span>

Let’s have a look at what this does. The following is what the tree in my Media Library looks like with this turned on:

One thing I want to point out is that I have not rigorously tested this solution so use at your own risk.

Also, after discussing this solution with Sitecore MVPs Akshay Sura and Kamruz Jaman, Akshay brought up a great idea of having a custom MasterDataView being driving by the Sitecore Rules Engine. I thought it was a great idea, and maybe some day in the future I might blog about this but, honestly, I think this might be something that you, my dear reader, should have a stab at, and give back to the community if you find such a solution.

Until next time, keep on Sitecoring.

Magically Register Sitecore Configuration Objects in the Sitecore IoC Container using a custom Attribute

About a year and half ago, Sitecore MVP Alan Coates blogged about having Sitecore Configuration Objects (aka Config Objects) being sourced from the Sitecore IoC container, and then injected into service classes that need them. He did this on a theme of Helix Principles but in reality, the ability to create Config Objects has existed since the Configuration Factory was released on version 6.x — don’t ask me which exact version the Configuration Factory was released as this was many projects, many grey hairs, and many country moves ago albeit it’s not something new (you can search through some of my vintage blog posts all the way back to the beginning where I have used these), and the ability to stick these into the Sitecore IoC container has existed since version 8.2 — this is when Sitecore rolled out its IoC container with native Dependency Injection support.

However, he was correct on the statement that it is something which is often overlooked as I continue to Β―\_(ツ)_/Β― (/shrug on Slack πŸ˜‰ ) — or maybe even (γƒŽΰ² η›Šΰ² )γƒŽε½‘β”»β”β”» — when I see solutions where everyone dumps everything configuration-driven in a Sitecore setting. I hope this blog post will further reinforce the practice of employing config objects in your solutions — or hopefully make the Sitecore settings junkies adopt this alternative approach instead — especially when I’m going to discuss how I’ve been wiring-up these config objects into the Sitecore IoC container using a custom Attribute decorating classes which represent these Config Objects along with a Foundation layer module configurator to put these into the IoC container; this is an approach I have been doing for the past 2 years.

Virtually all of my posts in 2018 — see my last post of 2018 for an example — created and stuck Config Objects into the IoC container for injection into servlice classes where needed. I had done all of these using code which created and registered these into the IoC container in a Configurator for a particular Helix layer module. At the time, it felt a bit awkward to me as I felt the knowledge of the configuration path to these was a bit removed from the classes which represent these objects, so I came up with the following way to define these configuration paths closer to the class definitions of the Config Objects (you can’t really get any closer than having the path be somewhere on the class, and this is done through a custom Attribute).

First, we need to custom Attribute which will decorate classes which represent the Config Objects:

using System;

using Foundation.DependencyInjection.Enums;

namespace Foundation.DependencyInjection
{
    [AttributeUsage(AttributeTargets.Class, Inherited = false)]
    public class ServiceConfigObjectAttribute : Attribute
    {
        public Lifetime Lifetime { get; set; } = Lifetime.Singleton;

		public string ConfigPath { get; set; }

		public Type ServiceType { get; set; }
	}
}

This solution involves assembly scanning using wildcards. I adapted Kam Figy‘s solution around registering MvC Controllers into the IoC container (see the end of https://kamsar.net/index.php/2016/08/Dependency-Injection-in-Sitecore-8-2/ for this) to make some of this magic happen but created a class with interface to abstract some of this out:

using System.Collections.Generic;
using System.Reflection;

namespace Foundation.DependencyInjection.Services.Assemblies
{
    public interface IAssemblyRepository
    {
        IEnumerable<Assembly> GetAssemblies(IEnumerable<string> assemblyFilters);
        
        IEnumerable<Assembly> GetAssemblies();

        Assembly GetCallingAssembly();
    }
}
using System;
using System.Linq;
using System.Reflection;
using System.Collections.Generic;
using System.Text.RegularExpressions;

namespace Foundation.DependencyInjection.Services.Assemblies
{
    public class AssemblyRepository : IAssemblyRepository
    {
        public IEnumerable<Assembly> GetAssemblies(IEnumerable<string> assemblyFilters)
        {
            var assemblyNames = new HashSet<string>(assemblyFilters.Where(filter => !filter.Contains('*')));
            var wildcardNames = assemblyFilters.Where(filter => filter.Contains('*')).ToArray();

            return GetAssemblies().Where(assembly =>
            {
                var nameToMatch = assembly.GetName().Name;
                if (assemblyNames.Contains(nameToMatch)) return true;

                return wildcardNames.Any(wildcard => IsWildcardMatch(nameToMatch, wildcard));
            }).ToList();
        }

        protected virtual bool IsWildcardMatch(string input, string wildcards)
        {
            return Regex.IsMatch(input, "^" + Regex.Escape(wildcards).Replace("\\*", ".*").Replace("\\?", ".") + "$", RegexOptions.IgnoreCase);
        }

        public IEnumerable<Assembly> GetAssemblies() => AppDomain.CurrentDomain.GetAssemblies();

        public Assembly GetCallingAssembly() => Assembly.GetCallingAssembly();
    }
}

The class above returns a collection of Assemblies which match a collection of wildcards passed to it.

I then created a factory class with interface to create the class instance above:

namespace Foundation.DependencyInjection.Services.Assemblies.Factories
{
    public interface IAssemblyRepositoryFactory
    {
        IAssemblyRepository CreateAssemblyRepository();
    }
}
namespace Foundation.DependencyInjection.Services.Assemblies.Factories
{
    public class AssemblyRepositoryFactory : IAssemblyRepositoryFactory
    {
        public IAssemblyRepository CreateAssemblyRepository() => new AssemblyRepository();
    }
}

The class above has a method which creates the instance of the AssemblyRepository class instance as its interface.

Next, I create an enumeration to define the lifetimes of the service classes — I can’t think of a reason why you would want these Config Objects to be anything but a Singleton but who knows, you might have a reason:

namespace Foundation.DependencyInjection.Enums
{
    public enum Lifetime
    {
        Transient,
        Singleton,
        Scoped
    }
}

Following this, I created some Extension Methods of IServiceCollection so I can find all classes decorated with the ServiceConfigObjectAttribute class defined above; use the Sitecore Configuration Factory service to create their instances; and register them in the IoC container with the appropriate service type — if none is provided, it’ll use the class type — and lifetime defined:

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Web.Http;
using System.Web.Mvc;

using Microsoft.Extensions.DependencyInjection;

using Sitecore.Abstractions;

using Foundation.DependencyInjection.Enums;
using Foundation.DependencyInjection.Services.Assemblies;
using Foundation.DependencyInjection.Services.Assemblies.Factories;

namespace Foundation.DependencyInjection.Extensions
{
    public static class ServiceCollectionExtensions
    {
		private static readonly IAssemblyRepository _assemblyRepository;

        static ServiceCollectionExtensions()
        {
            _assemblyRepository = CreateAssemblyRepository();
        }

        private static IAssemblyRepository CreateAssemblyRepository() => CreateAssemblyRepositoryFactory()?.CreateAssemblyRepository();

        private static IAssemblyRepositoryFactory CreateAssemblyRepositoryFactory() => new AssemblyRepositoryFactory();
		
		public static void AddClassesWithServiceConfigObjectAttribute(this IServiceCollection serviceCollection, params string[] assemblyFilters)
		{
			var assemblies = GetAssemblies(assemblyFilters);
			serviceCollection.AddClassesWithServiceConfigObjectAttribute(assemblies);
		}

		public static Assembly[] GetAssemblies(IEnumerable<string> assemblyFilters) => _assemblyRepository.GetAssemblies(assemblyFilters)?.ToArray();
		
		public static void AddClassesWithServiceConfigObjectAttribute(this IServiceCollection serviceCollection, params Assembly[] assemblies)
        {
            var typesWithAttributes = assemblies
                .Where(assembly => !assembly.IsDynamic)
                .SelectMany(GetExportedTypes)
                .Where(type => !type.IsAbstract && !type.IsGenericTypeDefinition)
                .Select(type => new { type.GetCustomAttribute<ServiceConfigObjectAttribute>()?.ServiceType, ImplementationType = type, type.GetCustomAttribute<ServiceConfigObjectAttribute>()?.ConfigPath, type.GetCustomAttribute<ServiceConfigObjectAttribute>()?.Lifetime })
                .Where(t => t.Lifetime != null);

            foreach (var type in typesWithAttributes)
            {
                AddConfigObject(serviceCollection, type.ServiceType == null ? type.ImplementationType : type.ServiceType, type.Lifetime.Value, type.ConfigPath);
            }
        }
		
		private static void AddConfigObject(IServiceCollection serviceCollection, Type serviceType, Lifetime lifetime, string configPath)
        {
            if (serviceCollection == null || serviceType == null || string.IsNullOrWhiteSpace(configPath))
            {
                return;
            }

            serviceCollection.Add(CreateServiceDescriptor(serviceType, provider => CreateConfigObject(provider, configPath), GetServiceLifetime(lifetime)));
        }
		
		private static ServiceDescriptor CreateServiceDescriptor(Type serviceType, Func<IServiceProvider, object> factory, ServiceLifetime lifetime) => new ServiceDescriptor(serviceType, factory, lifetime);
		
		private static object CreateConfigObject(IServiceProvider provider, string configPath)
        {
            BaseFactory factory = provider.GetService<BaseFactory>();
            return factory.CreateObject(configPath, true);
        }
		
		private static ServiceLifetime GetServiceLifetime(Lifetime lifetime)
        {
            if (lifetime == Lifetime.Singleton)
            {
                return ServiceLifetime.Singleton;
            }

            if (lifetime == Lifetime.Transient)
            {
                return ServiceLifetime.Transient;
            }

            return ServiceLifetime.Transient;
        }
	}
}

Now, we need to have a configurator to call the extension method in the class above. I first defined a base configurator which will have a method to return the assembly wildcards — it’s virtual just in case you need to target different assemblies (I’m sure there’s a better place to put these wildcards but I stuck them here for now):

namespace Foundation.DependencyInjection.Configurators
{
    public abstract class BaseAssemblyFiltersServicesConfigurator : IServicesConfigurator
    {
		private static readonly string[] _defaultAssemblyFilters = new string[] { "Foundation.*", "Feature.*" };

		protected virtual string[] GetAssemblyFilters() => _defaultAssemblyFilters;
    }
}

The following configurator inherits the base configurator above, and just calls the AddClassesWithServiceConfigObjectAttribute() extension method defined in the ServiceCollectionExtensions class above, and passes the assembly wildcard collection defined in the BaseAssemblyFiltersServicesConfigurator class above:

using Microsoft.Extensions.DependencyInjection;

using Foundation.DependencyInjection.Extensions;

namespace Foundation.DependencyInjection.Configurators
{
    public class ServiceConfigObjectAttributeServicesConfigurator : BaseAssemblyFiltersServicesConfigurator
	{
		public override void Configure(IServiceCollection serviceCollection) => serviceCollection.AddClassesWithServiceConfigObjectAttribute(GetAssemblyFilters());
	}
}

I then registered the configurator above in a 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>
    <services>
        <configurator type= "Foundation.DependencyInjection.Configurators.ServiceConfigObjectAttributeServicesConfigurator, Foundation.DependencyInjection"/>
    </services>
  </sitecore>
</configuration>

Alright, now that we have all of this out of the way, let’s see how we can use all of the stuff above.

I created all of the following for a future blog post — and also for a project I had recently worked on — but I’ll show these now to demonstrate how this all works, and to also save me time on writing that future post πŸ˜‰

The following class is a Config Object which has the name of a Sheer UI client command, and a delay value for that command to be invoked inside the Sitecore client:

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

namespace Foundation.Kernel.Models.Client
{
	[ServiceConfigObject(ConfigPath = "moduleSettings/foundation/kernel/clientServiceSettings", Lifetime = Lifetime.Singleton)]
	public class ClientServiceSettings
	{
		public string LoadItemClientCommandName { get; set; }

		public int LoadItemClientCommandDelay { get; set; }
	}
}

Here’s what it looks like in a 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>
	  <moduleSettings>
		  <foundation>
			  <kernel>
				  <clientServiceSettings type="Foundation.Kernel.Models.Client.ClientServiceSettings, Foundation.Kernel" singleInstance="true">
					  <LoadItemClientCommandName>item:load</LoadItemClientCommandName>
					  <LoadItemClientCommandDelay>1</LoadItemClientCommandDelay>
				  </clientServiceSettings>
			  </kernel>
		  </foundation>
	  </moduleSettings>
  </sitecore>
</configuration>

Let’s define another Config Object but something which is a little more “complex” than the example above.

I created the following class to represent a Sheer UI command — ultimately, these will be stored in a Dictionary so I can find a command format by key:

namespace Foundation.Kernel.Models.Client
{
	public class Command
	{
		public string Name { get; set; }

		public string CommandFormat { get; set; }
	}
}

Next, I need a service type/implementation to manage Command class instances above, and ultimately wrap a Dictionary containing information for these Commands:

using Foundation.Kernel.Models.Client;

namespace Foundation.Kernel.Services.Client
{
	public interface IClientCommandService
	{
		string GetClientCommand(string name, params string[] arguments);

		Command GetCommand(string name);
	}
}

Here’s the implementation of the interface above:

using System.Collections.Generic;

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

using Foundation.Kernel.Models.Client;

namespace Foundation.Kernel.Services.Client
{
	[ServiceConfigObject(ConfigPath = "moduleSettings/foundation/kernel/clientCommandService ", ServiceType = typeof(IClientCommandService), Lifetime = Lifetime.Singleton)]
	public class ClientCommandService : IClientCommandService
	{
		private readonly IDictionary<string, Command> _commands = new Dictionary<string, Command>();

		protected void AddClientCommand(string key, Command command)
		{
			if(string.IsNullOrWhiteSpace(key) || command == null)
			{
				return;
			}

			_commands[key] = command;
		}

		public string GetClientCommand(string name, params string[] arguments)
		{
			string commandFormat = GetCommandFormat(name);
			if(string.IsNullOrWhiteSpace(commandFormat))
			{
				return string.Empty;
			}

			return string.Format(commandFormat, arguments);
		}

		protected virtual string GetCommandFormat(string name) => GetCommand(name)?.CommandFormat;

		public Command GetCommand(string name) => _commands[name];
	}
}

So if I were to call the GetClientCommand() method on the service class above like this:

IClientCommandService clientCommandService; // make pretend this was injected in a class that's using it
clientCommandService.GetClientCommand("item:load") // this would return item:load(id={0}) so that we can do a string.Format() on it while providing an Item ID (this is used to load (or reload) an Item in the Content Editor

We would get a item:load(id={0}) so that we could supply an Item ID to replace {0}.

Here’s the Sitecore patch configuration file which represents the Config Object above:

<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>
	  <services>
		  <configurator type="Foundation.Kernel.Configurators.KernelConfigurator, Foundation.Kernel"/>
	  </services>
	  <moduleSettings>
		  <foundation>
			  <kernel>
				  <clientCommandService type="Foundation.Kernel.Services.Client.ClientCommandService, Foundation.Kernel" singleInstance="true">
					  <Commands hint="list:AddClientCommand">
						  <command key="item:load" type="Foundation.Kernel.Models.Client.Command, Foundation.Kernel">
							  <Name>$(key)</Name>
							  <CommandFormat>item:load(id={0})</CommandFormat>
						  </command>
					  </Commands>
				  </clientCommandService>
			  </kernel>
		  </foundation>
	  </moduleSettings>
  </sitecore>
</configuration>

Here’s what both Config Objects looking like on /sitecore/admin/showservicesconfig.aspx after all the code above runs:

Registered in IoC container

Now that the two configuration objects are in the IoC container, we can inject them into the follow service. Here’s the interface of that service:

using Sitecore.Data;
using Sitecore.Data.Items;
using Sitecore.Web.UI.HtmlControls;

namespace Foundation.Kernel.Services.Client
{
	public interface IClientService
	{
		void RefreshItem(Item item);

		void RefreshItem(ID itemId);

		ClientCommand Timer(string eventName, int delay);

		void Alert(string message);
	}
}

Here’s the implementation the service:

using Sitecore.Data;
using Sitecore.Data.Items;
using Sitecore.Web.UI.HtmlControls;

using Foundation.Kernel.Services.Client.SheerUI;
using Foundation.Kernel.Services.UniqueIdentifier;
using Foundation.Kernel.Models.Client;

namespace Foundation.Kernel.Services.Client
{
	public class ClientService : IClientService
	{
		private readonly ClientServiceSettings _settings;
		private readonly IIDService _idService;
		private readonly ISheerResponseService _sheerResponseService;
		private readonly IClientCommandService _clientCommandService;
		private readonly IClientResponseService _clientResponseService;
		
		public ClientService(ClientServiceSettings settings, IIDService idService, ISheerResponseService sheerResponseService, IClientCommandService clientCommandService, IClientResponseService clientResponseService)
		{
			_settings = settings; // config object was injected here
			_idService = idService; 
			_sheerResponseService = sheerResponseService;
			_clientCommandService = clientCommandService; // another config object injected here 
			_clientResponseService = clientResponseService;
		}

		public void RefreshItem(Item item) => RefreshItem(item?.ID);

		public void RefreshItem(ID itemId)
		{
			if(IsNullOrEmpty(itemId))
			{
				return;
			}

			string command = GetClientCommand(GetLoadItemClientCommandName(), itemId.ToString());
			if(string.IsNullOrWhiteSpace(command))
			{
				return;
			}

			Timer(command, GetLoadItemClientCommandDelay());
		}

		protected virtual bool IsNullOrEmpty(ID id) => _idService.IsNullOrEmpty(id);

		protected virtual string GetLoadItemClientCommandName() => _settings.LoadItemClientCommandName;

		protected virtual string GetClientCommand(string name, params string[] arguments) => _clientCommandService.GetClientCommand(name, arguments);

		protected virtual int GetLoadItemClientCommandDelay() => _settings.LoadItemClientCommandDelay;

		public ClientCommand Timer(string eventName, int delay) => _clientResponseService.Timer(eventName, delay);

		public void Alert(string message) => _sheerResponseService.Alert(message);
	}
}

The class above consumes instances of the ClientServiceSettings and IClientCommandService Config Objects along with other injected services which I am omitting for brevity – I believe I may have covered these other service classes in previous blog posts, and may cover some others in future blog posts — but the basic idea of this class is to wrap methods to functionality of Sheer UI to send Alert messages, refresh an item in the content tree of the Contend Editor, or to invoke another client command (via the Timer() method).

Most of my posts moving forward will use the ServiceConfigObjectAttribute above so be sure to understand what’s going on here in order to fully know what’s going on in those future posts.

magic

Allow Text for Empty List Items from Dynamic Datasources in Sitecore Experience Forms

Last week, when building a form on Sitecore Experience Forms, I noticed an issue where you cannot associate text with an empty list option for List fields.

For example, suppose you have a Dropdown List field where you want to put “Please select blah blah blah”, you cannot do this as you cannot associate text with Empty List value options using the “out of the box” (OOTB) feature to add an empty list option, nor can you have an empty Value field on an Item when using a Dynamic datasource.

Let me create a dummy form to illustrate the issue (no, I’m not calling you a dummy πŸ˜‰ ). Let’s have fun with donuts!

Suppose we have the following donut items in a folder somewhere which will be used as list options in a Dropdown List field in Forms:

We also have one which will serve as the top option telling the user to choose a donut:

Well, OOTB, this Item with an empty value field will not be an option in my field:

Sure, you might say “Hey Mike, you can associate empty options in these fields by setting the ‘Empty list item at top of list’ property setting in Forms.”

Yes, you can do that but you cannot associate text with that empty option:

Β‘Esta no bueno!

Well, after some digging, I discovered the service Sitecore.ExperienceForms.Mvc.DataSource.IListDataSourceProvider in Sitecore.ExperienceForms.Mvc.dll which seemed like a good place to look (I’ve mentioned before that virtually everything on Sitecore Experience Forms is in the Sitecore IoC so you should have a look at /sitecore/admin/showservicesconfig.aspx when looking to customize it).

When I looked at this interface’s implementation, Sitecore.ExperienceForms.Mvc.DataSource.ListDataSourceProvider, I noticed ListFieldItem — a POCO which represents options for List Fields on Forms — are only added when their Value properties aren’t null or empty, so I decided to customize this to allow for those which have empty Value properties but not empty Text properties. The following solution does just that.

I first created the following POCO class to contain any hardcoded string values I saw in the OOTB service (please don’t hardcode things as you won’t be invited to parties if you do so πŸ˜‰ ). An instance of this class will be hydrated from values housed in a Sitecore configuration file further down in this post:

namespace Sandbox.Foundation.Form.Models.Mvc.Datasource
{
	public class ListDataSourceProviderSettings
	{
		public string ListDelimiters { get; set; }
	}
}

I then created the following configurator to hydrate the POCO instance above from the Sitecore configuration file further down. I ultimately stick this instance into the Sitecore IoC as a singleton so I can inject it into any service classes that need it:

using System;
 
using Microsoft.Extensions.DependencyInjection;
 
using Sitecore.Abstractions;
using Sitecore.DependencyInjection;
 
namespace Sandbox.Foundation.Form
{
    public class ListDataSourceFieldsConfigurator : IServicesConfigurator
    {
        public void Configure(IServiceCollection serviceCollection)
        {
            serviceCollection.AddSingleton(CreateListDataSourceProviderSettings);
        }
 
        private ListDataSourceProviderSettings CreateListDataSourceProviderSettings(IServiceProvider provider) => CreateConfigObject<ListDataSourceProviderSettings>(provider, "moduleSettings/foundation/form/listDataSourceProviderSettings");
 
        private TConfigObject CreateConfigObject<TConfigObject>(IServiceProvider provider, string path) where TConfigObject : class => GetService<BaseFactory>(provider).CreateObject(path, true) as TConfigObject;
        
        private TService GetService<TService>(IServiceProvider provider) => provider.GetService<TService>();
    }
}

Next, I created the following class to replace the OOTB one:

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

using Sitecore.Data.Items;
using Sitecore.ExperienceForms.Mvc.DataSource;
using Sitecore.ExperienceForms.Mvc.Models;
using Sitecore.ExperienceForms.Mvc.Pipelines;

using Sandbox.Foundation.Form.Models.Mvc.Datasource;

namespace Sandbox.Foundation.Form.Services.Mvc.Datasource
{
	public class AllowEmptyValuesListDataSourceProvider : ListDataSourceProvider
	{
		private readonly ListDataSourceProviderSettings _settings;
		private readonly IFormBuilderContext _formBuilderContext;
		private readonly IListItemParser _listItemParser;

		public AllowEmptyValuesListDataSourceProvider(ListDataSourceProviderSettings settings, IFormBuilderContext formBuilderContext, IListItemParser listItemParser)
			: base(formBuilderContext, listItemParser)
		{
			_settings = settings;
			_listItemParser = listItemParser;
		}

		public override IEnumerable<ListFieldItem> GetListItems(string dataSource, string displayFieldName, string valueFieldName, string defaultSelection)
		{
			IEnumerable<Item> items = GetDataItems(dataSource);
			if(items == null || !items.Any())
			{
				return CreateEmptyListFieldItemCollection();
			}

			IList<ListFieldItem> options = CreateNewListFieldItemList();
			if(options == null)
			{
				return CreateEmptyListFieldItemCollection();
			}

			IEnumerable<string> itemsToSelect = GetItemsToSelect(defaultSelection);
			foreach (Item item in items)
			{
				ListFieldItem listFieldItem = ParseListFieldItem(item, displayFieldName, valueFieldName);
				if (listFieldItem == null || string.IsNullOrWhiteSpace(listFieldItem.Text))
				{
					continue;
				}

				SetSelected(itemsToSelect, listFieldItem);
				options.Add(listFieldItem);
			}

			return options;
		}

		protected virtual IEnumerable<ListFieldItem> CreateEmptyListFieldItemCollection() => CreateEmptyCollection<ListFieldItem>();

		protected virtual IList<ListFieldItem> CreateNewListFieldItemList() => CreateNewList<ListFieldItem>();

		protected virtual IList<TObject> CreateNewList<TObject>() => new List<TObject>();

		protected virtual IEnumerable<string> GetItemsToSelect(string itemsToSelect)
		{
			char[] delimiters = GetListDelimiters();
			if (string.IsNullOrWhiteSpace(itemsToSelect) || !HasAny(delimiters))
			{
				return CreateEmptyCollection<string>();
			}

			return itemsToSelect.Split(delimiters);
		}

		protected virtual char[] GetListDelimiters()
		{
			if (string.IsNullOrWhiteSpace(_settings.ListDelimiters))
			{
				return CreateEmptyCollection<char>().ToArray();
			}

			return _settings.ListDelimiters.ToCharArray();
		}

		protected virtual IEnumerable<TObject> CreateEmptyCollection<TObject>() => Enumerable.Empty<TObject>();

		protected virtual ListFieldItem ParseListFieldItem(Item item, string displayFieldName, string valueFieldName) => _listItemParser.Parse(item, displayFieldName, valueFieldName);

		protected virtual void SetSelected(IEnumerable<string> itemsToSelect, ListFieldItem listFieldItem)
		{
			if(!HasAny(itemsToSelect) || string.IsNullOrWhiteSpace(listFieldItem?.ItemId))
			{
				return;
			}

			listFieldItem.Selected = ShouldSelect(itemsToSelect, listFieldItem);
		}

		protected virtual bool ShouldSelect(IEnumerable<string> itemsToSelect, ListFieldItem listFieldItem) => ContainsValue(itemsToSelect, listFieldItem?.ItemId);

		protected virtual bool ContainsValue(IEnumerable<string> items, string value) => HasAny(items) && !string.IsNullOrWhiteSpace(value) && items.Contains(value);

		protected virtual bool HasAny<TObject>(IEnumerable<TObject> collection) => collection != null && collection.Any();
	}
}

I’m not going to go much into how this class works as I think you should read it over a few times to discover for yourself on how it works. Ultimately, it will add ListFieldItem instances with empty Value properties but not those with empty Text properties.

I then tied it all together using the following Sitecore configuration file:

<?xml version="1.0"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/" xmlns:env="http://www.sitecore.net/xmlconfig/env">
  <sitecore>
	  <moduleSettings>
		  <foundation>
			  <form>
				  <listDataSourceProviderSettings type="Sandbox.Foundation.Form.Models.Mvc.Datasource.ListDataSourceProviderSettings, Sandbox.Foundation.Form" singleInstance="true">
					  <ListDelimiters>|</ListDelimiters>
				  </listDataSourceProviderSettings>
			  </form>
		  </foundation>
	  </moduleSettings>
	  <services>
		  <configurator type="Sandbox.Foundation.Form.ListDataSourceFieldsConfigurator, Sandbox.Foundation.Form"/>
		  <register serviceType="Sitecore.ExperienceForms.Mvc.DataSource.IListDataSourceProvider, Sitecore.ExperienceForms.Mvc">
			  <patch:attribute name="implementationType">Sandbox.Foundation.Form.Services.Mvc.Datasource.AllowEmptyValuesListDataSourceProvider, Sandbox.Foundation.Form</patch:attribute>
		  </register>
	  </services>
  </sitecore>
</configuration>

One thing to note is I’m replacing the OOTB service with my custom one above under /services/register[@serviceType=’Sitecore.ExperienceForms.Mvc.DataSource.IListDataSourceProvider, Sitecore.ExperienceForms.Mvc’]

After doing a build and deploy of my solution locally, I reloaded the Forms Builder/designer page. As you can see my empty valued option with text now displays:

Yep, it was fixed.

Yes, you can have your donut and eat it too. πŸ˜‰

Until next time, keep on Sitecoring.

Oh, and Happy Holidays as well!

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.

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

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

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

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

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

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

Anyways, let’s get started.

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

using System;

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

		public string LogEntryType { get; set; }

		public string LogMessage { get; set; }

		public string Message { get; set; }

		public object Owner { get; set; }

		public DateTime CreatedDate { get; set; }
	}
}

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

using System;

using Sandbox.Foundation.Forms.Models.Logging;

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

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

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

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

using System;

using Sandbox.Foundation.Forms.Models.Logging;

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

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

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

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

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

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

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

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

using System;

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

		public string LogMessage { get; set; }

		public DateTime Created { get; set; }
	}
}

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

The following interface is for factories of RepositoryLogEntry:

using System;

using Sandbox.Foundation.Forms.Models.Logging;

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

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

using System;

using Sandbox.Foundation.Forms.Models.Logging;

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

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

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

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

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

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

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

This next class implements the interface above:

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

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

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

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

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

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

		public string ErrorLogEntryType { get; set; }

		public string FatalLogEntryType { get; set; }

		public string InfoLogEntryType { get; set; }

		public string WarnLogEntryType { get; set; }

		public string ExceptionPrefix { get; set; }

		public string MessagePrefix { get; set; }

		public string SourcePrefix { get; set; }

		public string NestedExceptionPrefix { get; set; }

		public string LogEntryTimeFormat { get; set; }
	}
}

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

using Sandbox.Foundation.Forms.Models.Logging;

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

		string GetErrorLogEntryType();

		string GetFatalLogEntryType();

		string GetInfoLogEntryType();

		string GetWarnLogEntryType();

		string GenerateLogMessage(LogEntry entry);
	}
}

And here is its implementation class:

using System;
using System.Text;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

USE [ExperienceFormsSettings]
GO

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

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

I also sprinkled some magical database dust onto the table:

πŸ˜‰

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

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

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

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

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

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

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

		public string LogDatabaseConnectionStringName { get; set; }

		public string InsertLogEntrySqlFormat { get; set; }

		public string ConnectionStringNameColumnName { get; set; }

		public string FieldsPrefixColumnName { get; set; }

		public string FieldsIndexNameColumnName { get; set; }

		public int NotFoundOrdinal { get; set; }

		public string LogEntryTypeParameterName { get; set; }

		public string LogMessageParameterName { get; set; }

		public string CreatedParameterName { get; set; }
	}
}

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

using System;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

		protected virtual object GetDefaultOwner()
		{
			return this;
		}

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

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

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

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

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

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

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

			entry.LogMessage = _logEntryService.GenerateLogMessage(entry);

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

			SaveRepositoryLogEntry(repositoryEntry);
		}

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

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

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

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

			ExecuteNoResult(insertCommand);
		}

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

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

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

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

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

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

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

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

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

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

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

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

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

using Sitecore.ExperienceForms.Mvc.Pipelines.RenderField;

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

Here is its implementation class:

using System;

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

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

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

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

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

I’m sure you would never do such a thing, right? πŸ˜‰

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

using Sitecore.ExperienceForms.Mvc.Pipelines.RenderField;

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

Here is the implementation class for this:

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

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

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

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

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

			_logger.Info(message);
		}
	}
}

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

using System;

using Microsoft.Extensions.DependencyInjection;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Ok, let’s take this for a spin.

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

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

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

As you can see, it worked.

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

Until next time, have yourself a Sitecoretastic day!

Grab Sitecore Experience Forms Configuration Settings from a Custom SQL Server Database Table

Just the other day, I poking around Sitecore Experience Forms to see what’s customisable and have pretty much concluded virtually everything is.

morpheus

How?

Just have a look at http://[instance]/sitecore/admin/showservicesconfig.aspx of your Sitecore instance and scan for “ExperienceForms”. You will also discover lots of its service class are registered in Sitecore’s IoC container — such makes it easy to swap things out with your own service implementation classes as long as they implement those service types defined in the container.

Since I love to tinkering with all things in Sitecore — most especially when it comes to customising bits of it — I decided to have a crack at replacing IFormsConfigurationSettings — more specifically Sitecore.ExperienceForms.Configuration.IFormsConfigurationSettings in Sitecore.ExperienceForms.dll which represents Sitecore configuration settings for Experience Forms — as it appeared to be something simple enough to do.

The IFormsConfigurationSettings interface represents the following configuration settings:

config-settings-ootb

So, what did I do to customise it?

I wrote a bunch of code — it’s all in this post — which pulls these settings from a custom SQL Server database table.

in-da-db.gif

Why did I do that? Well, I did it because I could. πŸ˜‰

Years ago, long before my Sitecore days, I worked on ASP.NET Web Applications which had their configuration settings stored in SQL Server databases. Whether this was, or still is, a good idea is a discussion for another time though you are welcome to drop a comment below with your thoughts.

However, for the meantime, just roll with it as the point of this post is to show that you can customise virtually everything in Experience Forms, and I’m just showing one example.

I first created the following class which implements IFormsConfigurationSettings:

using Sitecore.ExperienceForms.Configuration;

namespace Sandbox.Foundation.Forms.Models.Configuration
{
	public class SandboxFormsConfigurationSettings : IFormsConfigurationSettings
	{
		public string ConnectionStringName { get; set; }

		public string FieldsPrefix { get; set; }

		public string FieldsIndexName { get; set; }
	}
}

You might be asking “Mike, why did you create an implementation class when Experience Forms already provides one ‘out of the box’?”

Well, all the properties defined in Sitecore.ExperienceForms.Configuration.IFormsConfigurationSettings — these are the same properties that you see in the implementation class above — lack mutators on them in the interface. My implementation class of IFormsConfigurationSettings adds them in — I hate baking method calls in property accessors as it doesn’t seem clean to me. Β―\_(ツ)_/Β―

When I had a look at the “out of the box” implementation class of IFormsConfigurationSettings, I discovered direct calls to the GetSetting() method on the Sitecore.Configuration.Settings static class — this lives in Sitecore.Kernel.dll — but that doesn’t help me with setting those properties, hence the custom IFormsConfigurationSettings class above.

Next, I used the following SQL script to create my custom settings database table:

USE [ExperienceFormsSettings]
GO

CREATE TABLE [dbo].[FormsConfigurationSettings](
	[FieldsPrefix] [nvarchar](max) NULL,
	[FieldsIndexName] [nvarchar](max) NULL,
	[ConnectionStringName] [nvarchar](max) NULL
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
GO

I then inserted the settings from the configuration file snapshot above into my new table (I’ve omitted the SQL insert statement for this):

config-database

Now we need a way to retrieve these settings from the database. The following interface will be for factory classes which create instances of Sitecore.Data.DataProviders.Sql.SqlDataApi — this lives in Sitecore.Kernel.dll — with the passed connection strings:

using Sitecore.Data.DataProviders.Sql;

namespace Sandbox.Foundation.Forms.Services.Factories
{
	public interface ISqlDataApiFactory
	{
		SqlDataApi CreateSqlDataApi(string connectionString);
	}
}

Well, we can’t do much with an interface without having some class implement it. The following class implements the interface above:

using Sitecore.Data.DataProviders.Sql;
using Sitecore.Data.SqlServer;

namespace Sandbox.Foundation.Forms.Services.Factories
{
	public class SqlDataApiFactory : ISqlDataApiFactory
	{
		public SqlDataApi CreateSqlDataApi(string connectionString)
		{
			if(string.IsNullOrWhiteSpace(connectionString))
			{
				return null;
			}

			return new SqlServerDataApi(connectionString);
		}
	}
}

It’s just creating an instance of the SqlServerDataApi class. Nothing special about it at all.

Ironically, I do have to save my own configuration settings in Sitecore Configuration — this would include the the connection string name to the database that contains my new table as well as a few other things.

An instance of the following class will contain these settings — have a look at /sitecore/moduleSettings/foundation/forms/repositorySettings in the Sitecore patch file near the bottom of this post but be sure to come back up here when you are finished πŸ˜‰ — and this instance will be put into the Sitecore IoC container so it can be injected in an instance of a class I’ll talk about further down in this post:

namespace Sandbox.Foundation.Forms.Models.Configuration
{
	public class RepositorySettings
	{
		public string ConnectionStringName { get; set; }

		public string GetSettingsSql { get; set; }

		public string ConnectionStringNameColumnName { get; set; }

		public string FieldsPrefixColumnName { get; set; }

		public string FieldsIndexNameColumnName { get; set; }

		public int NotFoundOrdinal { get; set; }
	}
}

I then defined the following interface for repository classes which retrieve IFormsConfigurationSettings instances:

using Sitecore.ExperienceForms.Configuration;

namespace Sandbox.Foundation.Forms.Repositories.Configuration
{
	public interface ISettingsRepository
	{
		IFormsConfigurationSettings GetFormsConfigurationSettings();
	}
}

Here’s the implementation class for the interface above:

using System;
using System.Data;
using System.Linq;

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

using Sandbox.Foundation.Forms.Models.Configuration;
using Sandbox.Foundation.Forms.Services.Factories;

namespace Sandbox.Foundation.Forms.Repositories.Configuration
{
	public class SettingsRepository : ISettingsRepository
	{
		private readonly RepositorySettings _repositorySettings;
		private readonly BaseSettings _settings;
		private readonly int _notFoundOrdinal;
		private readonly ILogger _logger;
		private readonly SqlDataApi _sqlDataApi;

		public SettingsRepository(RepositorySettings repositorySettings, BaseSettings settings, ILogger logger, ISqlDataApiFactory sqlDataApiFactory)
		{
			_repositorySettings = repositorySettings;
			_settings = settings;
			_notFoundOrdinal = GetNotFoundOrdinal();
			_logger = logger;
			_sqlDataApi = GetSqlDataApi(sqlDataApiFactory);
		}

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

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

		protected virtual string GetConnectionStringName()
		{
			return _repositorySettings.ConnectionStringName;
		}

		public IFormsConfigurationSettings GetFormsConfigurationSettings()
		{
			try
			{
				return _sqlDataApi.CreateObjectReader(GetSqlQuery(), GetParameters(), GetMaterializer()).FirstOrDefault();
			}
			catch (Exception ex)
			{
				LogError(ex);
				
			}

			return CreateFormsConfigurationSettingsNullObject();
		}

		protected virtual string GetSqlQuery()
		{
			return _repositorySettings.GetSettingsSql;
		}

		protected virtual object[] GetParameters()
		{
			return Enumerable.Empty<object>().ToArray();
		}

		protected virtual Func<IDataReader, IFormsConfigurationSettings> GetMaterializer()
		{
			return new Func<IDataReader, IFormsConfigurationSettings>(ParseFormsConfigurationSettings);
		}

		protected virtual IFormsConfigurationSettings ParseFormsConfigurationSettings(IDataReader dataReader)
		{
			return new SandboxFormsConfigurationSettings
			{
				ConnectionStringName = GetString(dataReader, GetConnectionStringNameColumnName()),
				FieldsPrefix = GetString(dataReader, GetFieldsPrefixColumnName()),
				FieldsIndexName = GetString(dataReader, GetFieldsIndexNameColumnName())
			};
		}

		protected virtual string GetString(IDataReader dataReader, string columnName)
		{
			if(dataReader == null || string.IsNullOrWhiteSpace(columnName))
			{
				return string.Empty;

			}

			int ordinal = GetOrdinal(dataReader, columnName);
			if(ordinal == _notFoundOrdinal)
			{
				return string.Empty;
			}

			return dataReader.GetString(ordinal);
		}

		protected virtual int GetOrdinal(IDataReader dataReader, string columnName)
		{
			if(dataReader == null || string.IsNullOrWhiteSpace(columnName))
			{
				return _notFoundOrdinal;
			}

			try
			{
				return dataReader.GetOrdinal(columnName);
			}
			catch(IndexOutOfRangeException)
			{
				return _notFoundOrdinal;
			}
		}

		protected virtual int GetNotFoundOrdinal()
		{
			return _repositorySettings.NotFoundOrdinal;
		}

		protected virtual void LogError(Exception exception)
		{
			_logger.LogError(ToString(), exception, this);
		}

		protected virtual string GetConnectionStringNameColumnName()
		{
			return _repositorySettings.ConnectionStringNameColumnName;
		}

		protected virtual string GetFieldsPrefixColumnName()
		{
			return _repositorySettings.FieldsPrefixColumnName;
		}

		protected virtual string GetFieldsIndexNameColumnName()
		{
			return _repositorySettings.FieldsIndexNameColumnName;
		}

		protected virtual IFormsConfigurationSettings CreateFormsConfigurationSettingsNullObject()
		{
			return new SandboxFormsConfigurationSettings
			{
				ConnectionStringName = string.Empty,
				FieldsIndexName = string.Empty,
				FieldsPrefix = string.Empty
			};
		}
	}
}

I’m not going to go into details of all the code above but will talk about some important pieces.

The GetFormsConfigurationSettings() method above creates an instance of the IFormsConfigurationSettings instance using the SqlDataApi instance created from the injected factory service — this was defined above — with the SQL query provided from configuration along with the GetMaterializer() method which just uses the ParseFormsConfigurationSettings() method to create an instance of the IFormsConfigurationSettings by grabbing data from the IDataReader instance.

Phew, I’m out of breath as that was a mouthful. πŸ˜‰

I then registered all of my service classes above in the Sitecore IoC container using the following the configurator — aka a class that implements the IServicesConfigurator interface:

using System;

using Microsoft.Extensions.DependencyInjection;

using Sitecore.Abstractions;
using Sitecore.DependencyInjection;
using Sitecore.ExperienceForms.Configuration;

using Sandbox.Foundation.Forms.Repositories.Configuration;
using Sandbox.Foundation.Forms.Models.Configuration;

using Sandbox.Foundation.Forms.Services.Factories;

namespace Sandbox.Foundation.Forms
{
	public class SettingsConfigurator : IServicesConfigurator
	{
		public void Configure(IServiceCollection serviceCollection)
		{
			serviceCollection.AddSingleton(provider => GetRepositorySettings(provider));
			serviceCollection.AddSingleton<ISqlDataApiFactory, SqlDataApiFactory>();
			serviceCollection.AddSingleton<ISettingsRepository, SettingsRepository>();
			serviceCollection.AddSingleton(provider => GetFormsConfigurationSettings(provider));
		}

		private RepositorySettings GetRepositorySettings(IServiceProvider provider)
		{
			return CreateConfigObject<RepositorySettings>(provider, "moduleSettings/foundation/forms/repositorySettings");
		}

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

		private IFormsConfigurationSettings GetFormsConfigurationSettings(IServiceProvider provider)
		{
			ISettingsRepository repository = GetService<ISettingsRepository>(provider);
			return repository.GetFormsConfigurationSettings();
		}

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

One thing to note is the GetRepositorySettings() method above uses the Configuration Factory — this is represented by the BaseFactory abstract class which lives in the Sitecore IoC container “out of the box” — to create an instance of the RepositorySettings class, defined further up in this post, using the settings in the following Sitecore patch file:

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
	<sitecore>
		<services>
			<configurator type="Sandbox.Foundation.Forms.SettingsConfigurator, Sandbox.Foundation.Forms" />
			<register serviceType="Sitecore.ExperienceForms.Configuration.IFormsConfigurationSettings, Sitecore.ExperienceForms">
				<patch:delete />
			</register>
		</services>

		<moduleSettings>
			<foundation>
				<forms>
					<repositorySettings type="Sandbox.Foundation.Forms.Models.Configuration.RepositorySettings, Sandbox.Foundation.Forms" singleInstance="true">
						<ConnectionStringName>ExperienceFormsSettings</ConnectionStringName>
						<GetSettingsSql>SELECT TOP (1) {0}ConnectionStringName{1},{0}FieldsPrefix{1},{0}FieldsIndexName{1} FROM {0}FormsConfigurationSettings{1}</GetSettingsSql>
						<ConnectionStringNameColumnName>ConnectionStringName</ConnectionStringNameColumnName>
						<FieldsPrefixColumnName>FieldsPrefix</FieldsPrefixColumnName>
						<FieldsIndexNameColumnName>FieldsIndexName</FieldsIndexNameColumnName>
						<NotFoundOrdinal>-1</NotFoundOrdinal> 
					</repositorySettings>
				</forms>
			</foundation>
		</moduleSettings>
	</sitecore>
</configuration> 

I want to point out that I’m deleting the Sitecore.ExperienceForms.Configuration.IFormsConfigurationSettings service from Sitecore’s configuration as I am adding it back in to the Sitecore IoC container with my own via the configurator above.

After deploying everything, I waited for my Sitecore instance to reload.

jones-testing.gif

Once Sitecore was responsive again, I navigated to “Forms” on the Sitecore Launch Pad and made sure everything had still worked as before.

It did.

Trust me, it did. πŸ˜‰

living-in-db

If you have any questions/comments on any of the above, or would just like to drop a line to say “hello”, then please share in a comment below.

Otherwise, until next time, keep on Sitecoring.

Encrypt Sitecore Experience Forms Data in Powerful Ways

Last week, I was honoured to co-present Sitecore Experience Forms alongside my dear friend — and fellow trollster πŸ˜‰ — Sitecore MVP Kamruz Jaman at SUGCON EU 2018. We had a blast showing the ins and outs of Experience Forms, and of course trolled a bit whilst on the main stage.

During our talk, Kamruz had mentioned the possibility of replacing the “Out of the Box” (OOTB) Sitecore.ExperienceForms.Data.IFormDataProvider — this lives in Sitecore.ExperienceForms.dll — whose class implementations serve as Repository objects for storing or retrieving from some datastore (in Experience Forms this is MS SQL Server OOTB) with another to encrypt/decrypt data when saving to or retrieving from the datastore.

Well, I had done something exactly like this for Web Forms for Marketers (WFFM) about five years ago — be sure to have a read my blog post on this before proceeding as it gives context to the rest of this blog post — so thought it would be appropriate for me to have a swing at doing this for Experience Forms.

I first created an interface for classes that will encrypt/decrypt strings — this is virtually the same interface I had used in my older post on encrypting data in WFFM:

namespace Sandbox.Foundation.Forms.Services.Encryption
{
	public interface IEncryptor
	{
		string Encrypt(string key, string input);

		string Decrypt(string key, string input);
	}
}

I then created a class to encrypt/decrypt strings using the RC2 encryption algorithm — I had also poached this from my older post on encrypting data in WFFM (please note, this encryption algorithm is not the most robust so do not use this in any production environment. Please be sure to use something more robust):

using System.Text;
using System.Security.Cryptography;

namespace Sandbox.Foundation.Forms.Services.Encryption
{
	public class RC2Encryptor : IEncryptor
	{
		public string Encrypt(string key, string input)
		{
			byte[] inputArray = UTF8Encoding.UTF8.GetBytes(input);
			RC2CryptoServiceProvider rc2 = new RC2CryptoServiceProvider();
			rc2.Key = UTF8Encoding.UTF8.GetBytes(key);
			rc2.Mode = CipherMode.ECB;
			rc2.Padding = PaddingMode.PKCS7;
			ICryptoTransform cTransform = rc2.CreateEncryptor();
			byte[] resultArray = cTransform.TransformFinalBlock(inputArray, 0, inputArray.Length);
			rc2.Clear();
			return System.Convert.ToBase64String(resultArray, 0, resultArray.Length);
		}

		public string Decrypt(string key, string input)
		{
			byte[] inputArray = System.Convert.FromBase64String(input);
			RC2CryptoServiceProvider rc2 = new RC2CryptoServiceProvider();
			rc2.Key = UTF8Encoding.UTF8.GetBytes(key);
			rc2.Mode = CipherMode.ECB;
			rc2.Padding = PaddingMode.PKCS7;
			ICryptoTransform cTransform = rc2.CreateDecryptor();
			byte[] resultArray = cTransform.TransformFinalBlock(inputArray, 0, inputArray.Length);
			rc2.Clear();
			return UTF8Encoding.UTF8.GetString(resultArray);
		}
	}
}

Next, I created the following class to store settings I need for encrypting and decrypting data using the RC2 algorithm class above:

namespace Sandbox.Foundation.Forms.Models
{
	public class FormEncryptionSettings
	{
		public string EncryptionKey { get; set; }
	}
}

The encryption key above is needed for the RC2 algorithm to encrypt/decrypt data. I set this key in a config object defined in a Sitecore patch configuration file towards the bottom of this post.

I then created an interface for classes that will encrypt/decrypt FormEntry instances (FormEntry objects contain submitted data from form submissions):

using Sitecore.ExperienceForms.Data.Entities;

namespace Sandbox.Foundation.Forms.Services.Encryption
{
	public interface IFormEntryEncryptor
	{
		void EncryptFormEntry(FormEntry entry);

		void DecryptFormEntry(FormEntry entry);
	}
}

The following class implements the interface above:

using System.Linq;

using Sitecore.ExperienceForms.Data.Entities;

using Sandbox.Foundation.Forms.Models;

namespace Sandbox.Foundation.Forms.Services.Encryption
{
	public class FormEntryEncryptor : IFormEntryEncryptor
	{
		private readonly FormEncryptionSettings _formEncryptionSettings;
		private readonly IEncryptor _encryptor;

		public FormEntryEncryptor(FormEncryptionSettings formEncryptionSettings, IEncryptor encryptor)
		{
			_formEncryptionSettings = formEncryptionSettings;
			_encryptor = encryptor;
		}

		public void EncryptFormEntry(FormEntry entry)
		{
			if (!HasFields(entry))
			{
				return;
			}

			foreach (FieldData field in entry.Fields)
			{
				EncryptField(field);
			}
		}

		protected virtual void EncryptField(FieldData field)
		{
			if(field == null)
			{
				return;
			}

			field.FieldName = Encrypt(field.FieldName);
			field.Value = Encrypt(field.Value);
			field.ValueType = Encrypt(field.ValueType);
		}

		protected virtual string Encrypt(string input)
		{
			return _encryptor.Encrypt(_formEncryptionSettings.EncryptionKey, input);
		}

		public void DecryptFormEntry(FormEntry entry)
		{
			if (!HasFields(entry))
			{
				return;
			}

			foreach (FieldData field in entry.Fields)
			{
				DecryptField(field);
			}
		}

		protected virtual bool HasFields(FormEntry entry)
		{
			return entry != null
					&& entry.Fields != null
					&& entry.Fields.Any();
		}

		protected virtual void DecryptField(FieldData field)
		{
			if(field == null)
			{
				return;
			}

			field.FieldName = Decrypt(field.FieldName);
			field.Value = Decrypt(field.Value);
			field.ValueType = Decrypt(field.ValueType);
		}

		protected virtual string Decrypt(string input)
		{
			return _encryptor.Decrypt(_formEncryptionSettings.EncryptionKey, input);
		}
	}
}

The EncryptFormEntry() method above iterates over all FieldData objects contained on the FormEntry instance, and passes them to the EncryptField() mehod which encrypts the FieldName, Value and ValueType properties on them.

Likewise, the DecryptFormEntry() method iterates over all FieldData objects contained on the FormEntry instance, and passes them to the DecryptField() mehod which decrypts the same properties mentioned above.

I then created an interface for classes that will serve as factories for IFormDataProvider instances:

using Sitecore.ExperienceForms.Data;
using Sitecore.ExperienceForms.Data.SqlServer;

namespace Sandbox.Foundation.Forms.Services.Factories
{
	public interface IFormDataProviderFactory
	{
		IFormDataProvider CreateNewSqlFormDataProvider(ISqlDataApiFactory sqlDataApiFactory);
	}
}

The following class implements the interface above:

using Sitecore.ExperienceForms.Data;
using Sitecore.ExperienceForms.Data.SqlServer;

namespace Sandbox.Foundation.Forms.Services.Factories
{
	public class FormDataProviderFactory : IFormDataProviderFactory
	{
		public IFormDataProvider CreateNewSqlFormDataProvider(ISqlDataApiFactory sqlDataApiFactory)
		{
			return new SqlFormDataProvider(sqlDataApiFactory);
		}
	}
}

The CreateNewSqlFormDataProvider() method above does exactly was the method name says. You’ll see it being used in the following class below.

This next class ultimately becomes the new IFormDataProvider instance but decorates the OOTB one which is created from the factory class above:

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


using Sitecore.ExperienceForms.Data;
using Sitecore.ExperienceForms.Data.Entities;
using Sitecore.ExperienceForms.Data.SqlServer;

using Sandbox.Foundation.Forms.Services.Encryption;
using Sandbox.Foundation.Forms.Services.Factories;

namespace Sandbox.Foundation.Forms.Services.Data
{
	public class FormEncryptionDataProvider : IFormDataProvider
	{
		private readonly IFormDataProvider _innerProvider;
		private readonly IFormEntryEncryptor _formEntryEncryptor;

		public FormEncryptionDataProvider(ISqlDataApiFactory sqlDataApiFactory, IFormDataProviderFactory formDataProviderFactory, IFormEntryEncryptor formEntryEncryptor)
		{
			_innerProvider = CreateInnerProvider(sqlDataApiFactory, formDataProviderFactory);
			_formEntryEncryptor = formEntryEncryptor;
		}

		protected virtual IFormDataProvider CreateInnerProvider(ISqlDataApiFactory sqlDataApiFactory, IFormDataProviderFactory formDataProviderFactory)
		{
			return formDataProviderFactory.CreateNewSqlFormDataProvider(sqlDataApiFactory);
		}

		public void CreateEntry(FormEntry entry)
		{
			EncryptFormEntryField(entry);
			_innerProvider.CreateEntry(entry);
		}

		protected virtual void EncryptFormEntryField(FormEntry entry)
		{
			_formEntryEncryptor.EncryptFormEntry(entry);
		}

		public void DeleteEntries(Guid formId)
		{
			_innerProvider.DeleteEntries(formId);
		}

		public IReadOnlyCollection<FormEntry> GetEntries(Guid formId, DateTime? startDate, DateTime? endDate)
		{
			IReadOnlyCollection<FormEntry>  entries = _innerProvider.GetEntries(formId, startDate, endDate);
			if(entries == null || !entries.Any())
			{
				return entries;
			}

			foreach(FormEntry entry in entries)
			{
				DecryptFormEntryField(entry);
			}

			return entries;
		}

		protected virtual void DecryptFormEntryField(FormEntry entry)
		{
			_formEntryEncryptor.DecryptFormEntry(entry);
		}
	}
}

The class above does delegation to the IFormEntryEncryptor instance to encrypt the FormEntry data and then passes the FormEntry to the inner provider for saving.

For decrypting, it retrieves the data from the inner provider, and then decrypts it via the IFormEntryEncryptor instance before returning to the caller.

Finally, I created an IServicesConfigurator class to wire everything up into the Sitecore container (I hope you are using Sitecore Dependency Injection capabilities as this comes OOTB — there are no excuses for not using this!!!!!!):

using System;

using Microsoft.Extensions.DependencyInjection;

using Sitecore.Abstractions;
using Sitecore.DependencyInjection;
using Sitecore.ExperienceForms.Data;

using Sandbox.Foundation.Forms.Models;
using Sandbox.Foundation.Forms.Services.Encryption;
using Sandbox.Foundation.Forms.Services.Data;
using Sandbox.Foundation.Forms.Services.Factories;

namespace Sandbox.Foundation.Forms
{
	public class FormsServicesConfigurator : IServicesConfigurator
	{
		public void Configure(IServiceCollection serviceCollection)
		{
			serviceCollection.AddSingleton(provider => GetFormEncryptionSettings(provider));
			serviceCollection.AddSingleton<IEncryptor, RC2Encryptor>();
			serviceCollection.AddSingleton<IFormEntryEncryptor, FormEntryEncryptor>();
			serviceCollection.AddSingleton<IFormDataProviderFactory, FormDataProviderFactory>();
			serviceCollection.AddSingleton<IFormDataProvider, FormEncryptionDataProvider>();
		}

		private FormEncryptionSettings GetFormEncryptionSettings(IServiceProvider provider)
		{
			return CreateConfigObject<FormEncryptionSettings>(provider, "moduleSettings/foundation/forms/formEncryptionSettings");
		}

		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>();
		}
	}
}

Everything above is normal service class registration except for the stuff in the GetFormEncryptionSettings() method. Here, I’m creating an instance of a FormEncryptionSettings class but am instantiating it using the Sitecore Configuration Factory for the configuration object defined in the Sitecore patch configuration file below, and am making that available for being injected into classes that need it (the FormEntryEncryptor above uses it).

I then wired everything together using the following Sitecore patch configuration file:

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
	<sitecore>
		<services>
			<configurator type="Sandbox.Foundation.Forms.FormsServicesConfigurator, Sandbox.Foundation.Forms" />
			<register serviceType="Sitecore.ExperienceForms.IFormDataProvider, Sitecore.ExperienceForms">
				<patch:delete />
			</register>
		</services>
		<moduleSettings>
			<foundation>
				<forms>
					<formEncryptionSettings type="Sandbox.Foundation.Forms.Models.FormEncryptionSettings, Sandbox.Foundation.Forms" singleInstance="true">
						<!-- I stole this from https://sitecorejunkie.com/2013/06/21/encrypt-web-forms-for-marketers-fields-in-sitecore/ -->
						<EncryptionKey>88bca90e90875a</EncryptionKey>
					</formEncryptionSettings>
				</forms>
			</foundation>
		</moduleSettings>
	</sitecore>
</configuration>

I want to call out that I’m deleting the OOTB IFormDataProvider above using a patch:delete. I’m re-adding it via the IServicesConfigurator above using the decorator class previously shown above.

Let’s take this for a spin.

I first created a new form (this is under “Forms” on the Sitecore Launchepad ):

I then put it on a page with an MVC Layout; published everything; navigated to the test page with the form created above; filled out the form; and then clicked the submit button:

Let’s see if the data was encrypted. I opened up SQL Server Management Studio and ran a query on the FormEntry table in my Experience Forms Database:

As you can see the data was encrypted.

Let’s export the data to make sure it gets decrypted. We can do that by exporting the data as a CSV from Forms in the Sitecore Launchpad:

As you can see the data is decrypted in the CSV:

I do want to mention that Sitecore MVP JoΓ£o Neto had provided two other methods for encrypting data in Experience Forms in a post he wrote last January. I recommend having a read of that.

Until next time, see you on the Sitecore Slack πŸ˜‰

Display How Many Bucketed Items Live Within a Sitecore Item Bucket Using a Custom DataView

Over the past few weeks — if you haven’t noticed — I’ve been having a blast experimenting with Sitecore Item Buckets. It seems new ideas on what to build for it keep flooding my thoughts everyday. πŸ˜€

However, the other day an old idea that I wanted to solve a while back bubbled its way up into the forefront of my consciousness: displaying the count of Bucketed Items which live within each Item Bucket in the Content Tree.

I’m sure someone has built something to do this before though I didn’t really do any research on it as I was up for the challenge.

darth-vader-didnt-read

In all honesty, I enjoy spending my nights after work and on weekends building things in Sitecore — even if someone has built something like it before — as it’s a great way to not only discover new treasures hidden within the Sitecore assemblies, but also improve my programming skills — the saying “you lose it if you don’t use it” applies here.

nerds

You might be asking “Mike, we don’t store that many Sitecore Items in our Item Buckets; I can just go count them all by hand”.

Well, if that’s the case for you then you might want to reconsider why you are using the Item Buckets feature.

However, in theory, thousands if not millions of Items can live within an Item Bucket in Sitecore. If counting by hand is your thing — or even writing some sort of “script” (I’m not referring to PowerShell scripts that you would write using Sitecore PowerShell Extensions (SPE) — I definitely recommend harnessing all of the power this module has to offer — but instead to standalone ASP.NET Web Forms which some people erroneously call “scripts”) to generate some kind of report, then by all means go for it.

counting

That’s just not how I roll.

aint-no-time-for-that

So how are we going to display these counts to the user? We are ultimately going to create a custom Sitecore DataView.

If you aren’t familiar with DataViews in Sitecore, they basically allow you to change how Items are displayed in the Sitecore Content Tree.

I’m not going to go too much into details of how these work. I recommend having a read of the following posts by two fellow Sitecore MVPs for more information and to see other examples:

I do want to warn you: there is a lot of code in this post.

arghhhhh

You might want to go get a snack for this as it might take a while to get through all the code that I am showing here. Don’t worry, I’ll wait for you to get back.

eat-popcorn

Anyways, let’s jump right into it.

partay-meow

For this feature, I want to add a checkbox toggle in the Sitecore Ribbon to give users the ability turn this feature on and off.

bucketed-items-count-view-ribbon

In order to save the state of this checkbox, I defined the following interface:

namespace Sitecore.Sandbox.Web.UI.HtmlControls.Registries
{
    public interface IRegistry
    {
        bool GetBool(string key);

        bool GetBool(string key, bool defaultvalue);

        int GetInt(string key);

        int GetInt(string key, int defaultvalue);

        string GetString(string key);

        string GetString(string key, string defaultvalue);

        string GetValue(string key);

        void SetBool(string key, bool val);

        void SetInt(string key, int val);

        void SetString(string key, string value);

        void SetValue(string key, string value);
    }
}

Classes of the above interface will keep track of settings which need to be stored somewhere.

The following class implements the interface above:

namespace Sitecore.Sandbox.Web.UI.HtmlControls.Registries
{
    public class Registry : IRegistry
    {
        public virtual bool GetBool(string key)
        {
            return Sitecore.Web.UI.HtmlControls.Registry.GetBool(key);
        }

        public virtual bool GetBool(string key, bool defaultvalue)
        {
            return Sitecore.Web.UI.HtmlControls.Registry.GetBool(key, defaultvalue);
        }

        public virtual int GetInt(string key)
        {
            return Sitecore.Web.UI.HtmlControls.Registry.GetInt(key);
        }

        public virtual int GetInt(string key, int defaultvalue)
        {
            return Sitecore.Web.UI.HtmlControls.Registry.GetInt(key, defaultvalue);
        }

        public virtual string GetString(string key)
        {
            return Sitecore.Web.UI.HtmlControls.Registry.GetString(key);
        }

        public virtual string GetString(string key, string defaultvalue)
        {
            return Sitecore.Web.UI.HtmlControls.Registry.GetString(key, defaultvalue);
        }

        public virtual string GetValue(string key)
        {
            return Sitecore.Web.UI.HtmlControls.Registry.GetValue(key);
        }

        public virtual void SetBool(string key, bool val)
        {
            Sitecore.Web.UI.HtmlControls.Registry.SetBool(key, val);
        }

        public virtual void SetInt(string key, int val)
        {
            Sitecore.Web.UI.HtmlControls.Registry.SetInt(key, val);
        }

        public virtual void SetString(string key, string value)
        {
            Sitecore.Web.UI.HtmlControls.Registry.SetString(key, value);
        }

        public virtual void SetValue(string key, string value)
        {
            Sitecore.Web.UI.HtmlControls.Registry.SetValue(key, value);
        }
    }
}

I’m basically wrapping calls to methods on the static Sitecore.Web.UI.HtmlControls.Registry class which is used for saving state on the checkboxes in the Sitecore ribbon — it might be used for keeping track of other things in the Sitecore Content Editor though that is beyond the scope of this post. Nothing magical going on here.

I then defined the following interface for keeping track of Content Editor settings for things related to Item Buckets:

namespace Sitecore.Sandbox.Buckets.Settings
{
    public interface IBucketsContentEditorSettings
    {
        bool ShowBucketedItemsCount { get; set; }

        bool AreItemBucketsEnabled { get; }
    }
}

The ShowBucketedItemsCount boolean property lets the caller know if we are to show the Bucketed Items count, and the AreItemBucketsEnabled boolean property lets the caller know if the Item Buckets feature is enabled in Sitecore.

The following class implements the interface above:

using Sitecore.Diagnostics;

using Sitecore.Sandbox.Determiners.Features;
using Sitecore.Sandbox.Web.UI.HtmlControls.Registries;

namespace Sitecore.Sandbox.Buckets.Settings
{
    public class BucketsContentEditorSettings : IBucketsContentEditorSettings
    {
        protected IFeatureDeterminer ItemBucketsFeatureDeterminer { get; set; }
        
        protected IRegistry Registry { get; set; }

        protected string ShowBucketedItemsCountRegistryKey { get; set; }

        public bool ShowBucketedItemsCount
        {
            get
            {
                return ShouldShowBucketedItemsCount();
            }
            set
            {
                ToggleShowBucketedItemsCount(value);
            }
        }

        public bool AreItemBucketsEnabled
        {
            get
            {
                return GetAreItemBucketsEnabled();
            }
        }

        protected virtual bool ShouldShowBucketedItemsCount()
        {
            if (!AreItemBucketsEnabled)
            {
                return false;
            }

            EnsureRegistryDependencies();
            return Registry.GetBool(ShowBucketedItemsCountRegistryKey, false);
        }

        protected virtual void ToggleShowBucketedItemsCount(bool turnOn)
        {
            
            if (!AreItemBucketsEnabled)
            {
                return;
            }

            EnsureRegistryDependencies();
            Registry.SetBool(ShowBucketedItemsCountRegistryKey, turnOn);
        }

        protected virtual void EnsureRegistryDependencies()
        {
            Assert.IsNotNull(Registry, "Registry must be defined in configuration!");
            Assert.IsNotNullOrEmpty(ShowBucketedItemsCountRegistryKey, "ShowBucketedItemsCountRegistryKey must be defined in configuration!");
        }

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

I’m injecting an IFeatureDeterminer instance into the instance of the class above via the Sitecore Configuration Factory — have a look at the patch configuration file further down in this post — specifically the ItemBucketsFeatureDeterminer which is defined in a previous blog post. The IFeatureDeterminer instance determines whether the Item Buckets feature is turned on/off (I’m not going to repost that code here so if you haven’t seen this code, please go have a look now so you have an understanding of what it’s doing).

Its instance is used in the GetAreItemBucketsEnabled() method which just delegates to its IsEnabled() method and returns the value from that call. The GetAreItemBucketsEnabled() method is used in the get accessor of the AreItemBucketsEnabled property.

I’m also injecting an IRegistry instance into the instance of the class above — this is also defined in the patch configuration file further down — which is used for storing/retrieving the value of the ShowBucketedItemsCount property.

It is leveraged in the ShouldShowBucketedItemsCount() and ToggleShowBucketedItemsCount() methods where a boolean value is saved or retrieved, respectively, in the Sitecore Registry under a certain key — this key is also injected into the ShowBucketedItemsCountRegistryKey property via the Sitecore Configuration Factory.

So, we now have a way to keep track of whether we should display the Bucketed Items count. We just need a way to let the user turn this on/off. To do that, I need to create a custom Sitecore.Shell.Framework.Commands.Command.

Since Sitecore Commands are instantiated by the CreateObject() method on the MainUtil class (this lives in the Sitecore namespace in Sitecore.Kernel.dll and isn’t as advanced as the Sitecore.Configuration.Factory class as it won’t instantiate nested objects defined in configuration as does the Sitecore Configuration Factory), I built the following Command which will decorate Commands defined in Sitecore configuration:

using System.Xml;

using Sitecore.Configuration;
using Sitecore.Diagnostics;
using Sitecore.Shell.Framework.Commands;
using Sitecore.Web.UI.HtmlControls;
using Sitecore.Xml;

namespace Sitecore.Sandbox.Shell.Framework.Commands
{
    public class ExtendedConfigCommand : Command
    {
        private Command command;
        protected Command Command
        {
            get
            {
                if(command == null)
                {
                    command = GetCommand();
                    EnsureCommand();
                }

                return command;
            }
        }
        
        protected virtual Command GetCommand()
        {
            XmlNode currentCommandNode = Factory.GetConfigNode(string.Format("commands/command[@name='{0}']", Name));
            string configPath = XmlUtil.GetAttribute("extendedCommandPath", currentCommandNode);
            Assert.IsNotNullOrEmpty(configPath, string.Format("The extendedCommandPath attribute must be set {0}!", currentCommandNode));
            Command command = Factory.CreateObject(configPath, false) as Command;
            Assert.IsNotNull(command, string.Format("The command defined at '{0}' was either not properly set or is not an instance of Sitecore.Shell.Framework.Commands.Command. Double-check it!", configPath));
            return command;
        }

        protected virtual void EnsureCommand()
        {
            Assert.IsNotNull(Command, "GetCommand() cannot return a null Sitecore.Shell.Framework.Commands.Command instance!");
        }

        public override void Execute(CommandContext context)
        {
            Command.Execute(context);
        }

        public override string GetClick(CommandContext context, string click)
        {
            return Command.GetClick(context, click);
        }

        public override string GetHeader(CommandContext context, string header)
        {
            return Command.GetHeader(context, header);
        }

        public override string GetIcon(CommandContext context, string icon)
        {
            return Command.GetIcon(context, icon);
        }

        public override Control[] GetSubmenuItems(CommandContext context)
        {
            return Command.GetSubmenuItems(context);
        }

        public override string GetToolTip(CommandContext context, string tooltip)
        {
            return Command.GetToolTip(context, tooltip);
        }

        public override string GetValue(CommandContext context, string value)
        {
            return Command.GetValue(context, value);
        }

        public override CommandState QueryState(CommandContext context)
        {
            return Command.QueryState(context);
        }
    }
}

The GetCommand() method reads the XmlNode for the current command, and gets the value set on its extendedCommandPath attribute. This value must to be a config path defined under the <sitecore> element in Sitecore configuration.

If the attribute doesn’t exist or is empty, or a Command instance isn’t properly created, an exception is thrown.

Otherwise, it is set on the Command property on the class.

All methods here delegate to the same methods on the Command stored in the Command property.

I then defined the following Command which will be used by the checkbox we are adding to the Sitecore Ribbon:

using Sitecore.Diagnostics;
using Sitecore.Shell.Framework.Commands;
using Sitecore.Web.UI.Sheer;

using Sitecore.Sandbox.Buckets.Settings;

namespace Sitecore.Sandbox.Buckets.Shell.Framework.Commands
{
    public class ToggleBucketedItemsCountCommand : Command
    {
        protected IBucketsContentEditorSettings BucketsContentEditorSettings { get; set; }

        public override void Execute(CommandContext context)
        {
            if (!AreItemBucketsEnabled())
            {
                return;
            }
            
            ToggleShowBucketedItemsCount();
            Reload();
        }

        protected virtual void ToggleShowBucketedItemsCount()
        {
            Assert.IsNotNull(BucketsContentEditorSettings, "BucketsContentEditorSettings must be defined in configuration!");
            BucketsContentEditorSettings.ShowBucketedItemsCount = !BucketsContentEditorSettings.ShowBucketedItemsCount;
        }

        protected virtual void Reload()
        {
            SheerResponse.SetLocation(string.Empty);
        }

        public override CommandState QueryState(CommandContext context)
        {
            if(!AreItemBucketsEnabled())
            {
                return CommandState.Hidden;
            }

            if(!ShouldShowBucketedItemsCount())
            {
                return CommandState.Enabled;
            }

            return CommandState.Down;
        }

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

        protected virtual bool ShouldShowBucketedItemsCount()
        {
            Assert.IsNotNull(BucketsContentEditorSettings, "BucketsContentEditorSettings must be defined in configuration!");
            return BucketsContentEditorSettings.ShowBucketedItemsCount;
        }
    }
}

The QueryState() method determines whether we should display the checkbox — it will only be displayed if the Item Buckets feature is on — and what the state of the checkbox should be — if we are currently showing Bucketed Items count, the checkbox will be checked (this is represented by CommandState.Down). Otherwise, it will be unchecked (this is represented by CommandState.Enabled).

The Execute() method encapsulates the logic of what we are to do when the user checks/unchecks the checkbox. It’s basically delegating to the ToggleShowBucketedItemsCount() method to toggle the value of whether we are to display the Bucketed Items count, and then reloads the Content Editor to refresh the display in the Content Tree.

I then had to define this checkbox in the Core database:

bucketed-items-count-checkbox-core

I’m not going to go into details of how the above works as I’ve written over a gazillion posts on the subject. I recommend having a read of one of these older posts.

After going back to my Master database, I saw the new checkbox in the Sitecore Ribbon:

buckted-items-count-new-checkbox

Since we could be dealing with thousands — if not millions — of Bucketed Items for each Item Bucket, we need a performant way to grab the count of these Items. In this solution, I am leveraging the Sitecore.ContentSearch API to get these counts though needed to add some custom
Computed Index Field classes:

using Sitecore.Buckets.Managers;
using Sitecore.Configuration;
using Sitecore.ContentSearch;
using Sitecore.ContentSearch.ComputedFields;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;

using Sitecore.Sandbox.Buckets.Util.Methods;

namespace Sitecore.Sandbox.Buckets.ContentSearch.ComputedFields
{
    public class IsBucketed : AbstractComputedIndexField
    {
        protected IItemBucketsFeatureMethods ItemBucketsFeatureMethods { get; private set; }

        public IsBucketed()
        {
            ItemBucketsFeatureMethods = GetItemBucketsFeatureMethods();
            Assert.IsNotNull(ItemBucketsFeatureMethods, "GetItemBucketsFeatureMethods() cannot return null!");
        }

        protected virtual IItemBucketsFeatureMethods GetItemBucketsFeatureMethods()
        {
            IItemBucketsFeatureMethods methods = Factory.CreateObject("buckets/methods/itemBucketsFeatureMethods", false) as IItemBucketsFeatureMethods;
            Assert.IsNotNull(methods, "the IItemBucketsFeatureMethods instance was not defined properly in /sitecore/buckets/methods/itemBucketsFeatureMethods!");
            return methods;
        }

        public override object ComputeFieldValue(IIndexable indexable)
        {
            Item item = indexable as SitecoreIndexableItem;
            if (item == null)
            {
                return null;
            }

            
            return IsBucketable(item) && IsItemContainedWithinBucket(item);
        }

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

        protected virtual bool IsItemContainedWithinBucket(Item item)
        {
            Assert.ArgumentNotNull(item, "item");
            if(IsItemBucket(item))
            {
                return false;
            }

            return ItemBucketsFeatureMethods.IsItemContainedWithinBucket(item);
        }

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

            return true;
        }
    }
}

An instance of the class above ultimately determines if an Item is bucketed within an Item Bucket, and passes a boolean value to its caller denoting this via its ComputeFieldValue() method.

What determines whether an Item is bucketed? The code above says it’s bucketed only when the Item is bucketable and is contained within an Item Bucket.

The IsBucketable() method above ascertains whether the Item is bucketable by delegating to the IsBucketable() method on the BucketManager class in Sitecore.Buckets.dll.

The IsItemContainedWithinBucket() method determines if the Item is contained within an Item Bucket — you might be laughing as the name on the method is self-documenting — by delegating to the IsItemContainedWithinBucket() method on the IItemBucketsFeatureMethods instance — I’ve defined the code for this in this post so go have a look.

Moreover, the code does not consider Item Buckets to be Bucketed as that just doesn’t make much sense. πŸ˜‰ This would also give us an inaccurate count.

The following Computed Index Field’s ComputeFieldValue() method returns the string representation of the ancestor Item Bucket’s Sitecore.Data.ID for the Item — if it is contained within an Item Bucket:

using Sitecore.Configuration;
using Sitecore.ContentSearch;
using Sitecore.ContentSearch.ComputedFields;
using Sitecore.ContentSearch.Utilities;
using Sitecore.Data;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;

using Sitecore.Sandbox.Buckets.Util.Methods;

namespace Sitecore.Sandbox.Buckets.ContentSearch.ComputedFields
{
    public class ItemBucketAncestorId : AbstractComputedIndexField
    {
        protected IItemBucketsFeatureMethods ItemBucketsFeatureMethods { get; private set; }

        public ItemBucketAncestorId()
        {
            ItemBucketsFeatureMethods = GetItemBucketsFeatureMethods();
            Assert.IsNotNull(ItemBucketsFeatureMethods, "GetItemBucketsFeatureMethods() cannot return null!");
        }

        protected virtual IItemBucketsFeatureMethods GetItemBucketsFeatureMethods()
        {
            IItemBucketsFeatureMethods methods = Factory.CreateObject("buckets/methods/itemBucketsFeatureMethods", false) as IItemBucketsFeatureMethods;
            Assert.IsNotNull(methods, "the IItemBucketsFeatureMethods instance was not defined properly in /sitecore/buckets/methods/itemBucketsFeatureMethods!");
            return methods;
        }

        public override object ComputeFieldValue(IIndexable indexable)
        {
            Item item = indexable as SitecoreIndexableItem;
            if (item == null)
            {
                return null;
            }

            Item itemBucketAncestor = GetItemBucketAncestor(item);
            if(itemBucketAncestor == null)
            {

                return null;
            }

            return NormalizeGuid(itemBucketAncestor.ID);
        }

        protected virtual Item GetItemBucketAncestor(Item item)
        {
            Assert.ArgumentNotNull(item, "item");
            if(IsItemBucket(item))
            {
                return null;
            }

            Item itemBucket = ItemBucketsFeatureMethods.GetItemBucket(item);
            if(!IsItemBucket(itemBucket))
            {
                return null;
            }

            return itemBucket;
        }

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

            return true;
        }

        protected virtual string NormalizeGuid(ID id)
        {
            return IdHelper.NormalizeGuid(id);
        }
    }
}

Not to go too much into details of the class above, it will only return an Item Bucket’s Sitecore.Data.ID as a string if the Item lives within an Item Bucket and is not itself an Item Bucket.

If the Item is not within an Item Bucket or is an Item Bucket, null is returned to the caller via the ComputeFieldValue() method.

I then created the following subclass of Sitecore.ContentSearch.SearchTypes.SearchResultItem — this lives in Sitecore.ContentSearch.dll — in order to use the values in the index that the previous Computed Field Index classes returned for their storage in the search index:

using System.ComponentModel;

using Sitecore.ContentSearch;
using Sitecore.ContentSearch.Converters;
using Sitecore.ContentSearch.SearchTypes;
using Sitecore.Data;

namespace Sitecore.Sandbox.Buckets.ContentSearch.SearchTypes
{
    public class BucketedSearchResultItem : SearchResultItem
    {
        [IndexField("item_bucket_ancestor_id")]
        [TypeConverter(typeof(IndexFieldIDValueConverter))]
        public ID ItemBucketAncestorId { get; set; }

        [IndexField("is_bucketed")]
        public bool IsBucketed { get; set; }
    }
}

Now, we need a class to get the Bucketed Item count for an Item Bucket. I defined the following interface for class implementations that do just that:

using Sitecore.Data.Items;

namespace Sitecore.Sandbox.Buckets.Providers.Items
{
    public interface IBucketedItemsCountProvider
    {
        int GetBucketedItemsCount(Item itemBucket);
    }
}

I then created the following class that implements the interface above:

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

using Sitecore.Configuration;
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.Xml;

using Sitecore.Sandbox.Buckets.ContentSearch.SearchTypes;

namespace Sitecore.Sandbox.Buckets.Providers.Items
{
    public class BucketedItemsCountProvider : IBucketedItemsCountProvider
    {
        protected IDictionary<string, ISearchIndex> SearchIndexMap { get; private set; }

        public BucketedItemsCountProvider()
        {
            SearchIndexMap = CreateNewSearchIndexMap();
        }

        protected virtual IDictionary<string, ISearchIndex> CreateNewSearchIndexMap()
        {
            return new Dictionary<string, ISearchIndex>();
        }

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

            string databaseName = XmlUtil.GetAttribute("database", configNode, null);
            Assert.IsNotNullOrEmpty(databaseName, "The database attribute on the searchIndexMap configuration element cannot be null or the empty string!");
            Assert.ArgumentCondition(!SearchIndexMap.ContainsKey(databaseName), "database", "The searchIndexMap configuration element's database attribute values must be unique!");

            Database database = Factory.GetDatabase(databaseName);
            Assert.IsNotNull(database, string.Format("No database exists with the name of '{0}'! Make sure the database attribute on your searchIndexMap configuration element is set correctly!", databaseName));
            
            string searchIndexName = XmlUtil.GetAttribute("searchIndex", configNode, null);
            Assert.IsNotNullOrEmpty(searchIndexName, "The searchIndex attribute on the searchIndexMap configuration element cannot be null or the empty string!");

            ISearchIndex searchIndex = GetSearchIndex(searchIndexName);
            Assert.IsNotNull(searchIndex, string.Format("No search index exists with the name of '{0}'! Make sure the searchIndex attribute on your searchIndexMap configuration element is set correctly", searchIndexName));

            SearchIndexMap.Add(databaseName, searchIndex);
        }

        public virtual int GetBucketedItemsCount(Item bucketItem)
        {
            Assert.ArgumentNotNull(bucketItem, "bucketItem");

            ISearchIndex searchIndex = GetSearchIndex();
            using (IProviderSearchContext searchContext = searchIndex.CreateSearchContext())
            {
                var predicate = GetSearchPredicate<BucketedSearchResultItem>(bucketItem.ID);
                IQueryable<SearchResultItem> query = searchContext.GetQueryable<BucketedSearchResultItem>().Filter(predicate);
                SearchResults<SearchResultItem> results = query.GetResults();
                return results.Count();
            }
        }

        protected virtual ISearchIndex GetSearchIndex()
        {
            string databaseName = GetContentDatabaseName();
            Assert.IsNotNullOrEmpty(databaseName, "The GetContentDatabaseName() method cannot return null or the empty string!");
            Assert.ArgumentCondition(SearchIndexMap.ContainsKey(databaseName), "databaseName", string.Format("There is no ISearchIndex instance mapped to the database: '{0}'!", databaseName));
            return SearchIndexMap[databaseName];
        }

        protected virtual string GetContentDatabaseName()
        {
            Database database = Context.ContentDatabase ?? Context.Database;
            Assert.IsNotNull(database, "Argggggh! There's no content database! Houston, we have a problem!");
            return database.Name;
        }

        protected virtual ISearchIndex GetSearchIndex(string searchIndexName)
        {
            Assert.ArgumentNotNullOrEmpty(searchIndexName, "searchIndexName");
            return ContentSearchManager.GetIndex(searchIndexName);
        }

        protected virtual Expression<Func<TSearchResultItem, bool>> GetSearchPredicate<TSearchResultItem>(ID itemBucketId) where TSearchResultItem : BucketedSearchResultItem
        {
            Assert.ArgumentCondition(!ID.IsNullOrEmpty(itemBucketId), "itemBucketId", "itemBucketId cannot be null or empty!");
            var predicate = PredicateBuilder.True<TSearchResultItem>();
            predicate = predicate.And(item => item.ItemBucketAncestorId == itemBucketId);
            predicate = predicate.And(item => item.IsBucketed);
            return predicate;
        }
    }
}

Ok, so what’s going on in the class above? The AddSearchIndexMap() method is called by the Sitecore Configuration Factory to add database-to-search-index mappings — have a look at the patch configuration file further below. The code is looking up the appropriate search index for the content/context database.

The GetBucketedItemsCount() method gets the “predicate” from the GetSearchPredicate() method which basically says “Hey, I want an Item that has an ancestor Item Bucket Sitecore.Data.ID which is the same as the Sitecore.Data.ID passed to the method, and also this Item should be bucketed”.

The GetBucketedItemsCount() method then employs the Sitecore.ContentSearch API to get the result-set of the Items for the query, and returns the count of those Items.

Just as Commands, DataViews in Sitecore are instantiated by the CreateObject() method on MainUtil. I want to utilize the Sitecore Configuration Factory instead so that my nested configuration elements are instantiated and injected into my custom DataView. I built the following interface to make that possible:

using System.Collections;

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

namespace Sitecore.Sandbox.Web.UI.HtmlControls.DataViews
{
    public interface IDataViewBaseExtender
    {
        void FilterItems(ref ArrayList children, string filter);

        void GetChildItems(ItemCollection items, Item item);

        Database GetDatabase();

        Item GetItemFromID(string id, Language language, Version version);

        Item GetParentItem(Item item);

        bool HasChildren(Item item, string filter);

        void Initialize(string parameters);

        bool IsAncestorOf(Item ancestor, Item item);

        void SortItems(ArrayList children, string sortBy, bool sortAscending);
    }
}

All of the methods in the above interface correspond to virtual methods defined on the Sitecore.Web.UI.HtmlControl.DataViewBase class in Sitecore.Kernel.dll.

I then built the following abstract class which inherits from any DataView class that inherits from Sitecore.Web.UI.HtmlControl.DataViewBase:

using System.Collections;

using Sitecore.Collections;
using Sitecore.Configuration;
using Sitecore.Data;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Globalization;
using Sitecore.Web.UI.HtmlControls;

namespace Sitecore.Sandbox.Web.UI.HtmlControls.DataViews
{
    public abstract class ExtendedDataView<TDataView> : DataViewBase where TDataView : DataViewBase
    {
        protected IDataViewBaseExtender DataViewBaseExtender { get; private set; }

        protected ExtendedDataView()
        {
            DataViewBaseExtender = GetDataViewBaseExtender();
            EnsureDataViewBaseExtender();
        }

        protected virtual IDataViewBaseExtender GetDataViewBaseExtender()
        {
            string configPath = GetDataViewBaseExtenderConfigPath();
            Assert.IsNotNullOrEmpty(configPath, "GetDataViewBaseExtenderConfigPath() cannot return null or the empty string!");
            IDataViewBaseExtender dataViewBaseExtender = Factory.CreateObject(configPath, false) as IDataViewBaseExtender;
            Assert.IsNotNull(dataViewBaseExtender, string.Format("the IDataViewBaseExtender instance was not defined properly in '{0}'!", configPath));
            return dataViewBaseExtender;
        }

        protected abstract string GetDataViewBaseExtenderConfigPath();

        protected virtual void EnsureDataViewBaseExtender()
        {
            Assert.IsNotNull(DataViewBaseExtender, "GetDataViewBaseExtender() cannot return a null IDataViewBaseExtender instance!");
        }

        protected override void FilterItems(ref ArrayList children, string filter)
        {
            DataViewBaseExtender.FilterItems(ref children, filter);
        }

        protected override void GetChildItems(ItemCollection items, Item item)
        {
            DataViewBaseExtender.GetChildItems(items, item);
        }

        public override Database GetDatabase()
        {
            return DataViewBaseExtender.GetDatabase();
        }

        protected override Item GetItemFromID(string id, Language language, Version version)
        {
            return DataViewBaseExtender.GetItemFromID(id, language, version);
        }

        protected override Item GetParentItem(Item item)
        {
            return DataViewBaseExtender.GetParentItem(item);
        }

        public override bool HasChildren(Item item, string filter)
        {
            return DataViewBaseExtender.HasChildren(item, filter);
        }

        public override void Initialize(string parameters)
        {
            DataViewBaseExtender.Initialize(parameters);
        }

        public override bool IsAncestorOf(Item ancestor, Item item)
        {
            return DataViewBaseExtender.IsAncestorOf(ancestor, item);
        }

        protected override void SortItems(ArrayList children, string sortBy, bool sortAscending)
        {
            DataViewBaseExtender.SortItems(children, sortBy, sortAscending);
        }
    }
}

The GetDataViewBaseExtender() method gets the config path for the configuration-defined IDataViewBaseExtender — these IDataViewBaseExtender configuration definitions may or may not have nested configuration elements which will also be instantiated by the Sitecore Configuration Factory — from the abstract GetDataViewBaseExtenderConfigPath() method (subclasses must define this method).

The GetDataViewBaseExtender() then employs the Sitecore Configuration Factory to create this IDataViewBaseExtender instance, and return it to the caller (it’s being called in the class’ constructor).

If the instance is null, an exception is thrown.

All other methods in the above class delegate to methods with the same name and parameters on the IDataViewBaseExtender instance.

I then built the following subclass of the abstract class above:

using Sitecore.Web.UI.HtmlControls;

namespace Sitecore.Sandbox.Web.UI.HtmlControls.DataViews
{
    public class ExtendedMasterDataView : ExtendedDataView<MasterDataView>
    {
        protected override string GetDataViewBaseExtenderConfigPath()
        {
            return "extendedDataViews/extendedMasterDataView";
        }
    }
}

The above class is used for extending the MasterDataView in Sitecore.

It’s now time for the “real deal” DataView that does what we want: show the Bucketed Item counts for Item Buckets. The instance of the following class does just that:

using System.Collections;

using Sitecore.Buckets.Forms;
using Sitecore.Collections;
using Sitecore.Data;
using Sitecore.Data.Fields;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Globalization;

using Sitecore.Sandbox.Buckets.Providers.Items;
using Sitecore.Sandbox.Buckets.Settings;
using Sitecore.Sandbox.Buckets.Util.Methods;
using Sitecore.Sandbox.Web.UI.HtmlControls.DataViews;

namespace Sitecore.Sandbox.Buckets.Forms
{
    public class BucketedItemsCountDataView : BucketDataView, IDataViewBaseExtender
    {
        protected IBucketsContentEditorSettings BucketsContentEditorSettings { get; set; }

        protected IItemBucketsFeatureMethods ItemBucketsFeatureMethods { get; set; }

        protected IBucketedItemsCountProvider BucketedItemsCountProvider { get; set; }

        protected string SingularBucketedItemsDisplayNameFormat { get; set; }

        protected string PluralBucketedItemsDisplayNameFormat { get; set; }

        void IDataViewBaseExtender.FilterItems(ref ArrayList children, string filter)
        {
            FilterItems(ref children, filter);
        }

        void IDataViewBaseExtender.GetChildItems(ItemCollection children, Item parent)
        {
            GetChildItems(children, parent);
        }

        protected override void GetChildItems(ItemCollection children, Item parent)
        {
            
            base.GetChildItems(children, parent);
            if(!ShouldShowBucketedItemsCount())
            {
                return;
            }

            for (int i = children.Count - 1; i >= 0; i--)
            {
                Item child = children[i];
                if (IsItemBucket(child))
                {
                    int count = GetBucketedItemsCount(child);
                    Item alteredItem = GetCountDisplayNameItem(child, count);
                    children.RemoveAt(i);
                    children.Insert(i, alteredItem);
                }
            }
        }

        protected virtual bool ShouldShowBucketedItemsCount()
        {
            Assert.IsNotNull(BucketsContentEditorSettings, "BucketsContentEditorSettings must be defined in configuration!");
            return BucketsContentEditorSettings.ShowBucketedItemsCount;
        }

        protected virtual bool IsItemBucket(Item item)
        {
            Assert.IsNotNull(ItemBucketsFeatureMethods, "ItemBucketsFeatureMethods must be set in configuration!");
            Assert.ArgumentNotNull(item, "item");
            return ItemBucketsFeatureMethods.IsItemBucket(item);
        }

        protected virtual int GetBucketedItemsCount(Item itemBucket)
        {
            Assert.IsNotNull(BucketedItemsCountProvider, "BucketedItemsCountProvider must be set in configuration!");
            Assert.ArgumentNotNull(itemBucket, "itemBucket");
            return BucketedItemsCountProvider.GetBucketedItemsCount(itemBucket);
        }

        protected virtual Item GetCountDisplayNameItem(Item item, int count)
        {
            FieldList fields = new FieldList();
            item.Fields.ReadAll();
            
            foreach (Field field in item.Fields)
            {
                fields.Add(field.ID, field.Value);
            }

            int bucketedCount = GetBucketedItemsCount(item);
            string displayName = GetItemNameWithBucketedCount(item, bucketedCount);
            ItemDefinition itemDefinition = new ItemDefinition(item.ID, displayName, item.TemplateID, ID.Null);
            return new Item(item.ID, new ItemData(itemDefinition, item.Language, item.Version, fields), item.Database) { RuntimeSettings = { Temporary = true } };
        }

        protected virtual string GetItemNameWithBucketedCount(Item item, int bucketedCount)
        {
            Assert.IsNotNull(SingularBucketedItemsDisplayNameFormat, "SingularBucketedItemsDisplayNameFormat must be set in configuration!");
            Assert.IsNotNull(PluralBucketedItemsDisplayNameFormat, "PluralBucketedItemsDisplayNameFormat must be set in configuration!");

            if (bucketedCount == 1)
            {
                return ReplaceTokens(SingularBucketedItemsDisplayNameFormat, item, bucketedCount);
            }

            return ReplaceTokens(PluralBucketedItemsDisplayNameFormat, item, bucketedCount);
        }

        protected virtual string ReplaceTokens(string format, Item item, int bucketedCount)
        {
            Assert.ArgumentNotNullOrEmpty(format, "format");
            Assert.ArgumentNotNull(item, "item");
            string replaced = format;
            replaced = replaced.Replace("$displayName", item.DisplayName);
            replaced = replaced.Replace("$bucketedCount", bucketedCount.ToString());
            return replaced;
        }

        Database IDataViewBaseExtender.GetDatabase()
        {
            return GetDatabase();
        }

        Item IDataViewBaseExtender.GetItemFromID(string id, Language language, Version version)
        {
            return GetItemFromID(id, language, version);
        }

        Item IDataViewBaseExtender.GetParentItem(Item item)
        {
            return GetParentItem(item);
        }

        bool IDataViewBaseExtender.HasChildren(Item item, string filter)
        {
            return HasChildren(item, filter);
        }

        void IDataViewBaseExtender.Initialize(string parameters)
        {
            Initialize(parameters);
        }

        bool IDataViewBaseExtender.IsAncestorOf(Item ancestor, Item item)
        {
            return IsAncestorOf(ancestor, item);
        }

        void IDataViewBaseExtender.SortItems(ArrayList children, string sortBy, bool sortAscending)
        {
            SortItems(children, sortBy, sortAscending);
        }
    }
}

You might be saying to yourself “Mike, what in the world is going on here?” πŸ˜‰ Let me explain by starting with the GetChildItems() method.

The GetChildItems() method is used to build up the collection of child Items that display in the Content Tree when you expand a parent node. It does this by populating the ItemCollection instance passed to it.

The particular implementation above is delegating to the base class’ implementation to get the list of child Items for display in the Content Tree.

If we should not show the Bucketed Items count — this is determined by the ShouldShowBucketedItemsCount() method which just returns the boolean value set on the ShowBucketedItemsCount property of the injected IBucketsContentEditorSettings instance — the code just exits.

If we are to show the Bucketed Items count, we iterate over the ItemCollection collection and see if any of these child Items are Item Buckets — this is determined by the IsItemBucket() method.

If we find an Item Bucket, we get its count of Bucketed Items via the GetBucketedItemsCount() method which delegates to the GetBucketedItemsCount() method on the injected IBucketedItemsCountProvider instance.

Once we have the count, we call the GetCountDisplayNameItem() method which populates a FieldList collection with all of the fields defined on the Item Bucket; call the GetItemNameWithBucketedCount() method to get the new display name to show in the Content Tree — this method determines which display name format to use depending on whether we should use singular or pluralized messaging, and expands value on tokens via the ReplaceTokens() method — these tokens are defined in the patch configuration file below; creates an ItemDefinition instance so we can set the new display name; and returns a new Sitecore.Data.Items.Item instance to the caller.

No, don’t worry, we aren’t adding a new Item in the content tree but creating a fake “wrapper” of the real one, and replacing this in the ItemCollection.

We also have to fully implement the IDataViewBaseExtender interface. For most methods, I just delegate to the corresponding methods defined on the base class except for the IDataViewBaseExtender.GetChildItems() method which uses the GetChildItems() method defined above.

I then bridged everything above together via the following patch configuration file:

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <buckets>
      <extendedCommands>
        <toggleBucketedItemsCountCommand type="Sitecore.Sandbox.Buckets.Shell.Framework.Commands.ToggleBucketedItemsCountCommand, Sitecore.Sandbox" singleInstance="on">
          <BucketsContentEditorSettings ref="buckets/settings/bucketsContentEditorSettings" />
        </toggleBucketedItemsCountCommand>
      </extendedCommands>
      <providers>
        <items>
          <bucketedItemsCountProvider type="Sitecore.Sandbox.Buckets.Providers.Items.BucketedItemsCountProvider, Sitecore.Sandbox" singleInstance="true">
            <searchIndexMaps hint="raw:AddSearchIndexMap">
              <searchIndexMap database="master" searchIndex="sitecore_master_index" />
              <searchIndexMap database="web" searchIndex="sitecore_web_index" />
            </searchIndexMaps>
          </bucketedItemsCountProvider>
        </items>
      </providers>
      <settings>
        <bucketsContentEditorSettings type="Sitecore.Sandbox.Buckets.Settings.BucketsContentEditorSettings, Sitecore.Sandbox" singleInstance="true">
          <ItemBucketsFeatureDeterminer ref="determiners/features/itemBucketsFeatureDeterminer"/>
          <Registry ref="registries/registry" />
          <ShowBucketedItemsCountRegistryKey>/Current_User/UserOptions.View.ShowBucketedItemsCount</ShowBucketedItemsCountRegistryKey>
        </bucketsContentEditorSettings>
      </settings>
    </buckets>
    <commands>
      <command name="contenteditor:togglebucketeditemscount" type="Sitecore.Sandbox.Shell.Framework.Commands.ExtendedConfigCommand, Sitecore.Sandbox" extendedCommandPath="buckets/extendedCommands/toggleBucketedItemsCountCommand" />
    </commands>
    <contentSearch>
      <indexConfigurations>
        <defaultLuceneIndexConfiguration>
          <fieldMap>
            <fieldNames>
              <field fieldName="item_bucket_ancestor_id" storageType="YES" indexType="TOKENIZED" vectorType="NO" boost="1f" type="System.String" settingType="Sitecore.ContentSearch.LuceneProvider.LuceneSearchFieldConfiguration, Sitecore.ContentSearch.LuceneProvider">
                <analyzer type="Sitecore.ContentSearch.LuceneProvider.Analyzers.LowerCaseKeywordAnalyzer, Sitecore.ContentSearch.LuceneProvider" />
              </field>
              <field fieldName="is_bucketed" storageType="YES" indexType="TOKENIZED" vectorType="NO" boost="1f" type="System.Boolean" settingType="Sitecore.ContentSearch.LuceneProvider.LuceneSearchFieldConfiguration, Sitecore.ContentSearch.LuceneProvider" />
            </fieldNames>
          </fieldMap>
          <documentOptions>
            <fields hint="raw:AddComputedIndexField">
              <field fieldName="item_bucket_ancestor_id">Sitecore.Sandbox.Buckets.ContentSearch.ComputedFields.ItemBucketAncestorId, Sitecore.Sandbox</field>
              <field fieldName="is_bucketed">Sitecore.Sandbox.Buckets.ContentSearch.ComputedFields.IsBucketed, Sitecore.Sandbox</field>
            </fields>
          </documentOptions>
        </defaultLuceneIndexConfiguration>
      </indexConfigurations>
    </contentSearch>
    <dataviews>
      <dataview name="Master">
        <patch:attribute name="assembly">Sitecore.Sandbox</patch:attribute>
        <patch:attribute name="type">Sitecore.Sandbox.Web.UI.HtmlControls.DataViews.ExtendedMasterDataView</patch:attribute>
      </dataview>
    </dataviews>
    <extendedDataViews>
      <extendedMasterDataView type="Sitecore.Sandbox.Buckets.Forms.BucketedItemsCountDataView, Sitecore.Sandbox" singleInstance="true">
        <BucketsContentEditorSettings ref="buckets/settings/bucketsContentEditorSettings" />
        <ItemBucketsFeatureMethods ref="buckets/methods/itemBucketsFeatureMethods" />
        <BucketedItemsCountProvider ref="buckets/providers/items/bucketedItemsCountProvider" />
        <SingularBucketedItemsDisplayNameFormat>$displayName &lt;span style="font-style: italic; color: blue;"&gt;($bucketedCount bucketed item)&lt;span&gt;</SingularBucketedItemsDisplayNameFormat>
        <PluralBucketedItemsDisplayNameFormat>$displayName &lt;span style="font-style: italic; color: blue;"&gt;($bucketedCount bucketed items)&lt;span&gt;</PluralBucketedItemsDisplayNameFormat>
      </extendedMasterDataView>
    </extendedDataViews>
    <registries>
      <registry type="Sitecore.Sandbox.Web.UI.HtmlControls.Registries.Registry, Sitecore.Sandbox" singleInstance="true" />
    </registries>
  </sitecore>
</configuration>

bridge-collapse

Let’s see this in action:

bucketed-items-count-testing

As you can see, it is working as intended.

partay-hard

Magical, right?

magic

Well, not really — it just appears that way. πŸ˜‰

magic-not-really

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