Introduction
This article shows how to manipulate and dynamically create objects from classes that have not been referenced in your project yet.
Background
I had to create some Windows services, and I didn't want to modify my current project in the future. I realized that the best way was to create only a service server and divide all executing code into assemblies.
Using the Code
The main point is that all modules (executing assemblies) will contain a class which will be inherited from the base class (in our example, we will call it Module
). At this point, we want to execute all the modules in one single thread, so our base class can contain only one method called Run
.
public abstract class Module :
{
public RegisteredModule ModuleConfiguration
{
get;
set;
}
public List<Module> Children
{
get;
set;
}
protected bool isRunning;
public virtual bool IsRunning
{
get
{
return isRunning;
}
set
{
bool prevStatus = this.isRunning;
this.isRunning = value;
if (prevStatus != this.isRunning)
{
if (ModuleStatusChanged != null)
ModuleStatusChanged(this);
}
}
}
public abstract string Name
{
get;
set;
}
public event StatusChanged ModuleStatusChanged;
public virtual void AddChild(Module module)
{
throw new Exception("Cannot add to leaf!");
}
public virtual void RemoveChild(Module module)
{
throw new Exception("Cannot remove from leaf!");
}
protected abstract void Run();
public override string ToString()
{
return string.Format("{0} - {1}", this.Name,
this.IsRunning ? "Running" : "Stopped");
}
}
For better modules control, we will use the Composite design pattern (child management in this class, and generic lists), so we will create a class for encapsulating all our modules. This will be called CompositeModule
, and will have some function overriding.
public class CompositeModule : Module
{
public CompositeModule()
{
this.Children = new List<Module>();
}
public Module this[string name]
{
get
{
Module foundModule = this.Children.Find(delegate(Module m)
{ return m.Name == name; });
return foundModule;
}
}
public override bool IsRunning
{
get
{
bool running = false;
foreach (Module module in this.Children)
{
if (module.IsRunning)
running = true;
}
return running;
}
}
public override void AddChild(Module Module)
{
this.Children.Add(Module);
}
public override void RemoveChild(Module Module)
{
this.Children.Remove(Module);
}
protected override void Run()
{
foreach (Module module in this.Children)
{ module.Run();
}
}
public override string Name
{
get
{
return "Composite module";
}
set
{
}
}
}
The next step is to create a class that will be used by some concrete application. This class will work with CompositeModule
, and will be for raising events. This class is here for a simple purpose. Imagine a situation when we will have to modify our Windows service into a console app or some other type of project. Using this class will enable us to use a simple five line code to run our ServiceServer
wherever we want. The next step is to create a class for working with the config file where we will store information about our modules. E.g., what type and in which assembly is it located, if a module is enabled or disabled etc. The following code will show us how to create these classes:
[Serializable]
public class RegisteredModulesSection : ConfigurationSection
{
[ConfigurationProperty("modules")]
public RegisteredModules RegisteredModules
{
get
{
return (RegisteredModules)this["modules"];
}
set
{
this["modules"] = value;
}
}
}
[ConfigurationCollection(typeof(RegisteredModules), AddItemName = "registeredModule")]
public class RegisteredModules : ConfigurationElementCollection
{
protected override ConfigurationElement CreateNewElement()
{
return new RegisteredModule();
}
protected override object GetElementKey(ConfigurationElement element)
{
return ((RegisteredModule)element).Id;
}
public RegisteredModule this[int index]
{
get { return (RegisteredModule)base.BaseGet(index); }
}
public void Add(RegisteredModule module)
{
BaseAdd(module);
}
}
public class RegisteredModule : ConfigurationElement
{
[ConfigurationProperty("id", IsRequired = true)]
public string Id
{
get
{
return (string)this["id"];
}
set
{
this["id"] = value;
}
}
[ConfigurationProperty("name",IsRequired=true, DefaultValue="Module")]
public string Name
{
get
{
return (string)this["name"];
}
set
{
this["name"] = value;
}
}
[ConfigurationProperty("type", IsRequired = true)]
public string ClassType
{
get
{
return (string)this["type"];
}
set
{
this["type"] = value;
}
}
[ConfigurationProperty("assemblyName", IsRequired = true,
DefaultValue = "Module.dll")]
public string AssemblyName
{
get
{
return (string)this["assemblyName"];
}
set
{
this["assemblyName"] = value;
}
}
[ConfigurationProperty("moduleProperties", IsRequired = true)]
public ModuleProperties ModProperties
{
get
{
return (ModuleProperties)this["moduleProperties"];
}
set
{
this["moduleProperties"] = value;
}
}
}
public class ModuleProperties : ConfigurationElement
{
[ConfigurationProperty("enabled", IsRequired = true, DefaultValue = true)]
public bool Enabled
{
get
{
return (bool)this["enabled"];
}
set
{
this["enabled"] = value;
}
}
[ConfigurationProperty("description")]
public string Description
{
get
{
return (string)this["description"];
}
set
{
this["description"] = value;
}
}
}
And of course, do some modifications to the config file.
<configuration>
<configSections>
<section name="registeredModules"
type="Service.Configuration.RegisteredModulesSection,
Service.Configuration, Version=1.0.0.0,
Culture=neutral, PublicKeyToken=null" />
</configSections>
<registeredModules>
<modules>
<registeredModule id="1" name="SimpleModule1"
assemblyName="SimpleModule" type="SimpleModule.SimpleModule">
<moduleProperties enabled="true" description="SimpleModule" />
</registeredModule>
</modules>
</registeredModules>
</configuration>
Now, we will combine all these classes together in our main class called ServiceServer
, as we mentioned before. To ensure this class will be used only once in our appdomain, we will use the Singleton pattern for accessing the class.
public class ServiceServer
{
#region Singleton
private static ServiceServer instance;
public static ServiceServer Instance
{
get
{
if (instance == null)
instance = new ServiceServer();
return instance;
}
}
#endregion
public event ModuleChangedEvent ModuleChanged;
private CompositeModule rootModule;
public CompositeModule RootModule
{
get { return rootModule; }
set { rootModule = value; }
}
private ServiceServer()
{
rootModule = new CompositeModule();
}
public void LoadEnabledModules(RegisteredModules modules)
{
foreach (RegisteredModule regModule in modules)
{
if (regModule.ModProperties.Enabled)
{
RegisterModule(regModule);
}
}
}
private void RegisterModule(RegisteredModule regModule)
{
try
{
Assembly moduleAssembly = Assembly.Load(regModule.AssemblyName);
Type moduleType = moduleAssembly.GetType(regModule.ClassType);
if (moduleType != null)
{
Module module =
(Module)moduleAssembly.CreateInstance(regModule.ClassType);
module.Name = regModule.Name;
module.ModuleStatusChanged +=
new StatusChanged(module_ModuleStatusChanged);
this.rootModule.Children.Add(module);
}
}
catch
{
}
}
public void module_ModuleStatusChanged(Module sender)
{
if (this.ModuleChanged != null)
{
this.ModuleChanged(string.Format("{0}{1}",
sender.Name, sender.IsRunning));
}
}
public void ExecuteServer()
{
this.rootModule.Run();
}
}
And, the final step is to put this server calling into the main method of the application (Windows service) and create some simple module for testing.
Here is the service starting method:
protected override void OnStart(string[] args)
{
RegisteredModulesSection modules = null;
try
{
modules = ConfigurationManager.GetSection("registeredModules")
as RegisteredModulesSection;
}
catch
{
}
if (modules != null)
{
ServiceServer.Instance.LoadEnabledModules(modules.RegisteredModules);
ServiceServer.Instance.ExecuteServer();
}
Here is a simple module in the namespace SimpleModule
:
public class SimpleModule : Module
{
private int counter = 0;
public override string Name
{
get;
set;
}
protected override void Run()
{
counter++;
try
{
Log(string.Format("{0} run method fired {1}x",
this.Name, counter));
}
catch (Exception ex)
{
}
}
}
Of course, some additional steps are needed. E.g., we must declare some delegates for the events too.
Points of interest
For some kind of remote control of our server, it would be good to use .NET Remoting by adding a listener object to our solution.