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

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

Sitecore Technology MVP 2016
Sitecore MVP 2015
Sitecore MVP 2014

Enter your email address to follow this blog and receive notifications of new posts by email.

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


Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.