A Generic-Friendly PlugIn Framework in C#

Posted in software by Christopher R. Wirz on Mon May 13 2013



There are many posts online on how to create a C# PlugIn framework. This is not new. There are many great companies that build highly modular products on this very paradigm.

The goal of a PlugIn framework is to allow library files (DLLs) to be placed in a given given directory and become available to the main application.

The concept is simple: define an interface is one library and have a PlugIn publicly conform to that interface in a second library. This allows the first library to create instances of the classes defined in the second library - thus, a PlugIn is born.

Note: Interfaces on the main DLL must be public and classes on the PlugIn DLL which implement the interface must be public as well unless a PlugIn factory pattern is used.

So what's different? In many instances, a PlugIn provides both concrete types and classes to act on the type. This helps PlugIn developers maintain control over their intellectual property but still enhance the main application. Consider the following example provided by the main application:


public interface IDatagram : IComparable
{
	string Destination {get;}
	DateTime Timestamp {get;}
	byte[] Data {get;}
} 
public interface IDatagramSerializer<T> : IDisposable where T : IDatagram 
{
	T FromString(string str);
	string ToString(T data);
}

While on the surface you might think "this looks easy to serialize and deserialize", note that the properties on IDatagram are get-only. Often this is done to keep objects immutable. Classes are reference type and when reference type objects are passed into method, the method has the opportunity to make changes on the object. When class objects are immutable, these changes cannot occur.

Another reason is separation of responsibility. While a an IDatagram can have serialization methods such as


T FromString(string str);
string ToString(T data);
T FromXmlString(string str);
string ToXmlString(T data);
T FromJsonString(string str);
string ToJsonString(T data);
T FromBas64String(string str);
string ToBase64String(T data);

it becomes arduous to implement them all. Also, it leads to a lot of code bloat when most of the serialization methods are probably not needed.

Wouldn't it make more sense just to have


public class MyDataGram : IDatagram
{
	// Implementation goes here
} 
public class MyDataGramXmlSerializer : IDatagramSerializer<MyDataGram>
{
	MyDataGram FromString(string str) 
	{
		// Implementation goes here
	}
	string ToString(MyDataGram data)
	{
		// Implementation goes here
	}
}
public class GenericDataGramXmlSerializer<T> : IDatagramSerializer<T> where T : IDatagram
{
	T FromString(string str) 
	{
		// Implementation goes here
	}
	string ToString(T data)
	{
		// Implementation goes here
	}
}

such that you could get all the concrete data types as follows

// Get all the types conforming to an interface
var types = typeof(IDatagram).GetConformingTypes();
// Create an instant of the type (use the first with a parameterless constructor)
var obj = types?.FirstOrDefault(t => t.GetConstructors().Any(c => c.GetParameters().Count() == 0)).CreateInstanceOf();
// now find the serializers
var serializers = typeof(IDatagramSerializer<>).MakeGenericType(new Type[] {obj?.GetType()}).GetConformingTypes();
// Create a serializer (use the first with a parameterless constructor)
var objSerializer = serializers.FirstOrDefault(t => t.GetConstructors().Any(c => c.GetParameters().Count() == 0)).CreateInstanceOf();
// Get the string value
var stringValue = objSerializer?.ToString(obj);

The goal would be to get the serialiser most specific to serializing your data type. For example:

var myDataGramXmlSerializer = typeof(IDatagramSerializer<MyDataGram>).GetConformingTypes()
	.FirstOrDefault(t => t.GetConstructors().Any(c => c.GetParameters().Count() == 0)).CreateInstanceOf();

would give an instance of MyDataGramXmlSerializer.

It turns out, a little bit of code can do all that!


/// <summary>
///     Gets the DLL file paths for DLLs beginning with a certain prefix.
/// </summary>
/// <param name="path">The path of the directory containing the DLLs.</param>
/// <param name="prefix">The prefix.</param>
/// <returns>An array consisting of DLL paths</returns>
public static string[] GetDllNames(string path = null, string prefix = "")
{
	if (string.IsNullOrWhiteSpace(path))
	{
		path = Environment.CurrentDirectory;
	}
	var dllFileNames = new List<string>();
	if (!Directory.Exists(path)) return dllFileNames.ToArray();
	var di = new DirectoryInfo(path);
	dllFileNames.AddRange(from fi in di.GetFiles()
	  where
		  fi.Name.ToLower().StartsWith(prefix) && fi.Extension.ToLower().EndsWith("dll")
	  select fi.FullName);
	return dllFileNames.ToArray();
}

/// <summary>
///     Loads the DLL assemblies.
/// </summary>
/// <param name="path">The path.</param>
/// <param name="prefix">The prefix of the DLL</param>
/// <param name="includeCurrent">true to load the current assembly.</param>
/// <returns>An array of assemblies loaded from </returns>
internal static Assembly[] LoadDllAssemblies(string path = null, string prefix = "",
	bool includeCurrent = false)
{
	var assemblies = GetDllNames(path, prefix).Select(Assembly.LoadFrom).ToList();

	// Add in the current execution
	if (includeCurrent)
	{
		assemblies.AddRange(AppDomain.CurrentDomain.GetAssemblies());
	}

	return assemblies.Distinct().ToArray();
}

/// <summary>
///     Determines whether the type implements the interface type (which contains a generic).
/// </summary>
/// <param name="type">The type.</param>
/// <param name="interfaceType">Type of the interface.</param>
/// <returns>True if the type implements the interface type</returns>
public static bool IsGenericInterfaceType(this Type type, Type interfaceType)
{
	return type.GetInterfaces().Any(x => x.IsGenericType && x.GetGenericTypeDefinition() == interfaceType);
}

private static bool _hookdedInToAssemblyResolveHandler;

/// <summary>
///     Hooks the AssemblyResolveHandler in to assembly resolve event on the current AppDomain.
/// </summary>
/// <param name="unhook">if set to <c>true</c>, then attempt to unhook.</param>
internal static void HookInToAssemblyResolver(bool unhook = false)
{
	if (!_hookdedInToAssemblyResolveHandler && !unhook)
	{
		AppDomain.CurrentDomain.AssemblyResolve += AssemblyResolveHandler;
		_hookdedInToAssemblyResolveHandler = true;
	}
	else if (_hookdedInToAssemblyResolveHandler && unhook)
	{
		AppDomain.CurrentDomain.AssemblyResolve -= AssemblyResolveHandler;
		_hookdedInToAssemblyResolveHandler = false;
	}
}

private static readonly Dictionary<string, Assembly> Assemblies = new Dictionary<string, Assembly>();

/// <summary>
///     To be called when an AppDomain attempts to resolve an assembly
/// </summary>
/// <param name="sender">The sender.</param>
/// <param name="e">The <see cref="ResolveEventArgs" /> instance containing the event data.</param>
/// <returns>The first assembly to resolve.  Exception thrown if none.</returns>
/// <exception cref="System.ApplicationException">
///     Failed resolving assembly as the file could not be found.
///     or
///     Failed resolving assembly
/// </exception>
internal static Assembly AssemblyResolveHandler(object sender, ResolveEventArgs e)
{
	try
	{
		// Check for an assembly loaded before
		if (Assemblies.ContainsKey(e.Name))
		{
			return Assemblies[e.Name];
		}

		// Check for a current assembly
		var assemblyName = e.Name.Split(',')[0];
		var asm =
			AppDomain.CurrentDomain.GetAssemblies()
				.FirstOrDefault(a => a.GetTypes().Any(t => t.FullName == assemblyName));
		if (asm != null)
		{
			Assemblies[e.Name] = asm;
			return asm;
		}

		// Check for embedded resource
		var resourceName = new AssemblyName(e.Name).Name + ".dll";
		var resource = Array.Find(Assembly.GetExecutingAssembly().GetManifestResourceNames(),
			element => element.EndsWith(resourceName));

		if (!string.IsNullOrWhiteSpace(resource))
		{
			using (var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(resource))
			{
				if (stream != null)
				{
					var assemblyData = new byte[stream.Length];
					stream.Read(assemblyData, 0, assemblyData.Length);
					var embeddedAsm = Assembly.Load(assemblyData);
					if (embeddedAsm != null)
					{
						Assemblies[e.Name] = embeddedAsm;
						return embeddedAsm;
					}
				}
			}
		}

		// Check for a nearby DLL
		var assemblyBasePath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
		var fileName = Path.Combine(assemblyBasePath ?? "", assemblyName + ".dll");
		if (!File.Exists(fileName))
		{
			throw new ApplicationException(
				$"Failed resolving assembly as the file \"{fileName}\" could not be found.");
		}
		var dllAsm = Assembly.LoadFrom(fileName);
		Assemblies[e.Name] = dllAsm;
		return dllAsm;
	}
	catch (Exception ex)
	{
		throw new ApplicationException("Failed resolving assembly", ex);
	}
}

/// <summary>
///     Creates the instance of a given type.
/// </summary>
/// <param name="type">The type.</param>
/// <param name="parameters">The parameters to pass into the constructor.</param>
/// <returns>An instance of the type, or throws an exception</returns>
public static object CreateInstanceOf(this Type type, object[] parameters = null)
{
	HookInToAssemblyResolver();
	return AppDomain.CurrentDomain.CreateInstanceAndUnwrap(type.Assembly.FullName, type.FullName, true,
		BindingFlags.CreateInstance | BindingFlags.Public | BindingFlags.Instance, null, parameters, null, null);
}

/// <summary>
///     Gets all class types conforming to an interface.
/// </summary>
/// <param name="interfaceType">Type of the interface.</param>
/// <param name="path">The path.</param>
/// <param name="prefix">The prefix of the DLL</param>
/// <param name="includeCurrent">true to load the current assembly.</param>
/// <returns>A list of types</returns>
public static Type[] GetConformingTypes(this Type interfaceType, string path = null, string prefix = "", bool includeCurrent = false)
{
	// Load the assemblies
	var assemblies = LoadDllAssemblies(path: path, prefix: prefix, includeCurrent: includeCurrent);

	// Check if it conforms to a class
	if (!interfaceType.IsInterface)
	{
		// If so, just see if a type can be assigned to it
		return assemblies.SelectMany(s => s.GetTypes()).Where(interfaceType.IsAssignableFrom).ToArray();
	}

	var classTypes = assemblies.SelectMany(s => s.GetTypes()).Where(t =>
			// Return only class types
			t.IsClass &&
			// Names must be used as the original namespace is dropped
			t.GetInterfaces().Select(i => i.Name).Contains(interfaceType.Name)
		).ToList();

	// It must be an interface, so check if it's generic
	if (interfaceType.IsGenericType)
	{
		// Find classes which match exactly (when a class takes a generic and so does the interface)
		var exactMatch = classTypes.Where(
			t =>
				// Ensure the generic arguments are the same
				interfaceType.GetGenericArguments().Length == t.GetGenericArguments().Length
				&& t.GetGenericArguments().Intersect(interfaceType.GetGenericArguments()).Count() == t.GetGenericArguments().Length
		).ToList();
		if (exactMatch.Any())
		{
			// If there are any, return the matches
			var types = new List<Type>();
			foreach (var t in exactMatch)
			{
				try
				{
					types.Add(t);
				}
				catch
				{
					// Ignored
				}
			}
			if (types.Any()) { return types.ToArray(); }
		}

		// Find classes which match arguments
		var argMatch = classTypes.Where(
			t => t.GetInterfaces().Where(i => i.GetGenericArguments().Length > 0).All(f =>
			 interfaceType.GetGenericArguments().Length == f.GetGenericArguments().Length
				 && f.GetGenericArguments().Intersect(interfaceType.GetGenericArguments()).Count() == f.GetGenericArguments().Length)
		).ToList();
		if (argMatch.Any())
		{
			// If there are any, return the matches
			var types = new List<Type>();
			foreach (var t in argMatch)
			{
				try
				{
					types.Add(t);
				}
				catch
				{
					// Ignored
				}
			}
			if (types.Any()) { return types.ToArray(); }
		}

		// interfaceType.GetGenericArguments() == classTypes[1].GetInterfaces()[0].GetGenericArguments()

		// Check if there are any generic arguments
		if (interfaceType.GetGenericArguments().Length > 0)
		{
			// Find instances of types which can be passed the arguments
			var closeMatch = classTypes
				.Where(
					// Names must be used as the original namespace is dropped
					t => t.GetInterfaces().Select(i => i.Name).Contains(interfaceType.Name) &&
						 t.GetGenericArguments().Length == interfaceType.GetGenericArguments().Length
				).ToList();

			if (closeMatch.Any())
			{
				// If there are any, return the matches
				var types = new List<Type>();
				foreach (var t in closeMatch)
				{
					try
					{
						types.Add(t.MakeGenericType(interfaceType.GetGenericArguments()));
					}
					catch
					{
						// Ignored
					}
				}
				if (types.Any()) { return types.ToArray(); }
			}
		}
	}
	return assemblies.SelectMany(s => s.GetTypes()).Where(t => t.GetInterfaces().Contains(interfaceType) && t.IsClass).ToArray();
}

Now you can let PlugIn developers use generics!