Giving your user the opportunity to add new modules to your software on a plug & play basis during run-time improves the usability and functionality of you application marginally.
This blog will guide you on how to build your own plug & play system. The use-case we take here is a Data Acquisition module that allows us to add new Device Protocols at run-time. We have two parts to this solution – the parent application that handles data acquisition and the module device protocol. We need a shared project that allows the interaction between the two parts – introducing Interfaces.
A interface indicates the properties and methods available in a class but not its implementation which is defined in a class derived from the interface. The below interface IDevice is shared between the application and the module through a class library.
public interface IDevice
{
object Configuration { get; }
object Values { get; }
bool Start();
bool Stop();
}
Now lets build our two devices in a class library and compile it as separate DLLs. The IDevice class should be derived from the earlier class library we defined that is to be shared.
public class RTUDevice : IDevice
{
public object Configuration { get; }
public object Values { get; protected set; }
public RTUDevice(object configuration)
{
Configuration = configuration;
}
public bool Start() { }
public bool Stop() { }
}
public class TCPDevice : IDevice
{
public object Configuration { get; }
public object Values { get; protected set; }
public TCPDevice (object configuration)
{
Configuration = configuration;
}
public bool Start() { }
public bool Stop() { }
}
Now lets get to our application that support plug & play. For the purpose of this tutorial we will consider a new module is added to a folder that will be loaded from the application in run-time. We build this logic into our Factory Pattern that allows us to create objects for the given interface by loading them from the folder.
public static class DeviceFactory
{
private static ILogger _logger = Log.ForContext("SourceContext", "DeviceFactory");
private static readonly Dictionary<string, Type> Types = LoadDevicesFromAssembly(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Devices"));
private static Dictionary<string, Version> AssemblyVersions;
private static Dictionary<string, Type> LoadDevicesFromAssembly(string assemblyPath)
{
Dictionary<string, Type> deviceTypes = new Dictionary<string, Type>();
AssemblyVersions = new Dictionary<string, Version>();
IEnumerable<string> assemblyFiles = Directory.EnumerateFiles(assemblyPath, "*.dll", SearchOption.AllDirectories);
foreach (string assemblyFile in assemblyFiles)
{
try
{
Assembly assembly = Assembly.LoadFile(assemblyFile);
string name = assembly.GetName().Name;
Version version = assembly.GetName().Version;
if (AssemblyVersions.ContainsKey(name))
{
if (AssemblyVersions[name].CompareTo(version) > 0)
{
continue;
}
else
{
deviceTypes.Remove(name);
AssemblyVersions[name] = version;
}
}
else
{
AssemblyVersions.Add(name, version);
}
foreach (Type type in assembly.ExportedTypes)
{
try
{
if (type.IsClass && typeof(IDevice).IsAssignableFrom(type))
{
deviceTypes.Add(name, type);
}
}
catch (Exception ex)
{
_logger.Error(ex, "IDevice Assignable");
}
}
}
catch (Exception ex)
{
_logger.Error(ex, "Load Device Assembly");
}
}
return deviceTypes;
}
public static IDevice CreateDevice(
string type,
object configuration)
{
if (Types.ContainsKey(type))
{
try
{
IDevice device = Activator.CreateInstance(
Types[type],
new[] { configuration}) as IDevice;
return device;
}
catch (Exception ex)
{
_logger.Error(ex, "Create Device for Type:{type}", deviceConfiguration.Type);
return null;
}
}
else
{
_logger.Debug("Module not found for {type}", deviceConfiguration.Type);
return null;
}
}
}
Here we have added another feature that allows loading multiple versions of a module and the latest module can be used for initializing a class. However existing initialized objects with the older versions should be reinitialized.