Writing C# Plugin System - c#

I'm trying to write a plugin system to provide some extensibility to an application of mine so someone can write a plugin(s) for the application without touching the main application's code (and risk breaking something).
I've got the base "IPlugin" interface written (atm, nothing is implemented yet)
Here is how I'm loading:
public static void Load()
{
// rawr: http://www.codeproject.com/KB/cs/c__plugin_architecture.aspx
String[] pluginFiles = Directory.GetFiles(Plugins.PluginsDirectory, "*.dll");
foreach (var plugin in pluginFiles)
{
Type objType = null;
try
{
//Assembly.GetExecutingAssembly().GetName().Name
MessageBox.Show(Directory.GetCurrentDirectory());
Assembly asm = Assembly.Load(plugin);
if (asm != null)
{
objType = asm.GetType(asm.FullName);
if (objType != null)
{
if (typeof(IPlugin).IsAssignableFrom(objType))
{
MessageBox.Show(Directory.GetCurrentDirectory());
IPlugin ipi = (IPlugin)Activator.CreateInstance(objType);
ipi.Host = Plugins.m_PluginsHost;
ipi.Assembly = asm;
}
}
}
}
catch (Exception e)
{
MessageBox.Show(e.ToString(), "Unhandled Exception! (Please Report!)", System.Windows.Forms.MessageBoxButtons.OK, System.Windows.Forms.MessageBoxIcon.Information);
}
}
}
A friend tried to help but I really didn't understand what was wrong.
The folder structure for plugins is the following:
\
\Plugins\
All plugins reference a .dll called "Lab.Core.dll" in the [root] directory and it is not present in the Plugins directory because of duplicate references being loaded.
The plugin system is loaded from Lab.Core.dll which is also referenced by my executable. Type "IPlugin" is in Lab.Core.dll as well. Lab.Core.dll is, exactly as named, the core of my application.
EDIT:
Question: Why/What is that exception I'm getting and how could I go about fixing it?
FINAL EDIT:
Ok so I decided to re-write it after looking at some source code a friend wrote for a TF2 regulator.
Here's what I got and it works:
public class TestPlugin : IPlugin {
#region Constructor
public TestPlugin() {
//
}
#endregion
#region IPlugin Members
public String Name {
get {
return "Test Plugin";
}
}
public String Version {
get {
return "1.0.0";
}
}
public String Author {
get {
return "Zack";
}
}
public Boolean OnLoad() {
MessageBox.Show("Loaded!");
return true;
}
public Boolean OnAllLoaded() {
MessageBox.Show("All loaded!");
return true;
}
#endregion
}
public static void Load(String file) {
if (!File.Exists(file) || !file.EndsWith(".dll", true, null))
return;
Assembly asm = null;
try {
asm = Assembly.LoadFile(file);
} catch (Exception) {
// unable to load
return;
}
Type pluginInfo = null;
try {
Type[] types = asm.GetTypes();
Assembly core = AppDomain.CurrentDomain.GetAssemblies().Single(x => x.GetName().Name.Equals("Lab.Core"));
Type type = core.GetType("Lab.Core.IPlugin");
foreach (var t in types)
if (type.IsAssignableFrom((Type)t)) {
pluginInfo = t;
break;
}
if (pluginInfo != null) {
Object o = Activator.CreateInstance(pluginInfo);
IPlugin plugin = (IPlugin)o;
Plugins.Register(plugin);
}
} catch (Exception) {
}
}
public static void LoadAll() {
String[] files = Directory.GetFiles("./Plugins/", "*.dll");
foreach (var s in files)
Load(Path.Combine(Environment.CurrentDirectory, s));
for (Int32 i = 0; i < Plugins.List.Count; ++i) {
IPlugin p = Plugins.List.ElementAt(i);
try {
if (!p.OnAllLoaded()) {
Plugins.List.RemoveAt(i);
--i;
}
} catch (Exception) {
Plugins.List.RemoveAt(i);
--i;
}
}
}

The Managed Extensibility Framework (MEF) is a new library in .NET that enables greater reuse of applications and components. Using MEF, .NET applications can make the shift from being statically compiled to dynamically composed. If you are building extensible applications, extensible frameworks and application extensions, then MEF is for you.
http://www.codeplex.com/MEF
Edit: CodePlex is going away - the code has been moved to Github for archival purposes only: https://github.com/MicrosoftArchive/mef
MEF is now a part of the Microsoft .NET Framework, with types primarily under the System.Composition. namespaces. There are two versions of MEF
System.ComponentModel.Composition, which has shipped with .NET 4.0 and higher. This provides the standard extension model that has been used in Visual Studio. The documentation for this version of MEF can be found here
System.Compostion is a lightweight version of MEF, which has been optimized for static composition scenarios and provides faster compositions. It is also the only version of MEF that is a portable class library and can be used on phone, store, desktop and web applications. This version of MEF is available via NuGet and is documentation is available here

It sounds like you have a circular reference. You said your plugins reference Lab.Core.DLL, but you also say the plugins are loaded from Lab.Core.DLL.
Am I misunderstanding what is happening here?
EDIT: OK now that you have added your question to the question...
You need to have Lab.Core.DLL accessible to the plugin being loaded since it is a dependency. Normally that would mean having it in the same directory or in the GAC.
I suspect there are deeper design issues at play here, but this is your immediate problem.

As a side answer, i use these 2 interfaces for implementing that
public interface IPlugin {
string Name { get; }
string Description { get; }
string Author { get; }
string Version { get; }
IPluginHost Host { get; set; }
void Init();
void Unload();
IDictionary<int, string> GetOptions();
void ExecuteOption(int option);
}
public interface IPluginHost {
IDictionary<string, object> Variables { get; }
void Register(IPlugin plugin);
}

Related

How to access class of dynamically loaded clr / cli library in C#

I am novice in clr / cli and C#. I have one clr / cli library in my C# project. I want to load it dynamically and access the function of its class in C# . Can some one provide some example or right way to doing it.
Here is My header file of class declaration in Clr / cli library
namespace ManagedLibDarkClient {
public ref class AccountHandler
{
public:
AccountHandler()
{
}
static bool RegisterAccnt(String^ accountID, String^ authCode);
};
}
Please find below the function of my C# class on which I have tried to access it:--
private void RegisterWindow_ValidateEvent(object sender, ValidateEventArgs e)
{
Assembly assembly = Assembly.Loadfile("C:\\darkmailWindows\\darkmailwindows\\Dependencies\\ManagedLibDarkMail\\Lib\\ManagedLibDarkClient.dll");
if (assembly != null)
{
Type type = assembly.GetType("AccountHandler");
var obj = Activator.CreateInstance(type);
if (obj != null)
{
string[] args = { e.AccntInfo.AccntName, e.AccntInfo.AuthCode };
type.InvokeMember("RegisterAccnt", BindingFlags.Default | BindingFlags.InvokeMethod, null, obj, args);
}
else
MessageBox.Show("Unable to laod object");
}
else
MessageBox.Show("unable to load assembly");
}
}
Here in this example I am facing 2 issue :--
1- LoadFile hangs and did not return any thing.
2- I dont know how to get return value of my clr / cli function.
Here I would like to mention one more thing. I can access clr / cli if I link them statically. But I have to load it dynamically. It is crucial requirement for me.
First af all, regarding the loading issue, check that all the native dependencies (dlls) of your C++/CLI library are present in the working directory.
Make a third assembly containing an interface
public interface IAccountHandler
{
bool RegisterAccnt(String accountID, String authCode);
}
Add a reference to this assembly from both your projects, C++/CLI and C#
In C++/CLI:
public ref class AccountHandler : public IAccountHandler
{
public:
AccountHandler()
{
}
bool RegisterAccnt(String^ accountID, String^ authCode);
};
Then, in C#:
string filename = "C:\\darkmailWindows\\darkmailwindows\\Dependencies\\ManagedLibDarkMail\\Lib\\ManagedLibDarkClient.dll";
Assembly asm = Assembly.LoadFrom(filename);
foreach (Type t in asm.GetTypes())
{
if (t.GetInterfaces().Contains(typeof(IAccountHandler)))
{
try
{
IAccountHandler instance = (IAccountHandler)Activator.CreateInstance(t);
if (instance != null)
{
instance.RegisterAccnt(e.AccntInfo.AccntName, e.AccntInfo.AuthCode);
}
}
catch(Exception ex)
{
//manage exception
}
}
}
I think you don't need to make RegisterAccnt static.
You also can add the reference like you would do to link from a GAC registered or side by side assembly and manually handling the ResolveAssembly event when it fails loading. See the answer to this question.

Two Dlls Loaded with Assembly.LoadFrom Define the Same Class and Work Fine?

This is freaking me out, and I'm guessing it's because I'm severely misunderstanding something basic about how assemblies get loaded. I was not expecting this to work, can someone explain why it does?
Projects:
Plugins has the definition of a plugin class
Lib1 References Plugins and defines a plugin class
Lib2 References Plugins and defines a plugin class
Console references Plugins and looks for dlls near itself to load
Lib1 and Lib2 share a code file via symlink:
namespace Shared
{
public class SharedClass
{
public static string Key { get; set; }
}
}
Lib1 Plugin:
namespace Lib1
{
public class Lib1Plugin : Plugin
{
public override void Load()
{
SharedClass.Key = "Lib1 Key";
Console.WriteLine(SharedClass.Key);
}
public override void Run()
{
Console.WriteLine(SharedClass.Key);
}
}
}
Lib2 Plugin:
namespace Lib2
{
public class Lib2Plugin : Plugin
{
public override void Load()
{
SharedClass.Key = "Lib2 Key";
Console.WriteLine(SharedClass.Key);
}
public override void Run()
{
Console.WriteLine(SharedClass.Key);
}
}
}
Console:
static class Functions
{
public static IEnumerable<Type> FindDerivied(Assembly asm, Type baseType)
{
try
{
return asm.GetTypes().Where(t => baseType.IsAssignableFrom(t) && t != baseType);
}
catch (Exception e)
{
return new List<Type>();
}
}
}
class Program
{
static void Main(string[] args)
{
var di = new DirectoryInfo("Plugins");
var bos = new List<Plugin>();
if (di.Exists)
{
var dlls = di.EnumerateFiles();
foreach (var dll in dlls)
{
var asm = Assembly.LoadFrom(dll.FullName);
var builders = Functions.FindDerivied(asm, typeof(Plugin));
foreach (var builder in builders)
{
var bo = (Plugin)Activator.CreateInstance(builder);
bo.Load();
bos.Add(bo);
}
}
foreach (var bo in bos)
{
bo.Run();
}
var asms = AppDomain.CurrentDomain.GetAssemblies();
foreach (var asm in asms)
{
var exports =
asm.GetExportedTypes().Where(type => type.Name == "SharedClass")
.ToList();
foreach (var export in exports)
{
Console.WriteLine(export.FullName);
}
}
}
}
}
Output:
Lib1 Key
Lib2 Key
Lib1 Key
Lib2 Key
Shared.SharedClass
Shared.SharedClass
How does it know the difference!?
There is nothing preventing two assemblies from declaring types with identical fully-qualified names. Whether those types are similar or completely different (or here, are actually defined in the same source file) is irrelevant.
Although the page discussing extern alias uses "two versions of the same assembly" as it's motivating example, it's describing the general mechanism that would allow any consuming application to consume two (or more) libraries that declare types with identical fully-qualified type names.
You might have to reference two versions of assemblies that have the same fully-qualified type names. For example, you might have to use two or more versions of an assembly in the same application. By using an external assembly alias, the namespaces from each assembly can be wrapped inside root-level namespaces named by the alias, which enables them to be used in the same file.
And further, what this comes down to is that a fully-qualified type name, by itself, does not uniquely identify a specific type. A type's identity includes not just its name but also its assembly.
You did not share compiled code, but the file SharedClass.cs file. So the libraries don't know about each other's SharedClass, and hence there is nothing which should "know the difference". At compile time, each plugin gets linked to the SharedClass contained in the same assembly, and at runtime there are two SharedClasses which do not know anything about each other.

Is it possible to import another C# project with a main method and set it as the entry point?

Is it possible to create a project in C# without a main method, and import another project that has one, and set the entry point to be that main method of the imported project?
The purpose of this is to provide a library complete with its main method and all startup code, requiring only a couple of "plugin" methods. This will minimize boiler-plate (in particular, start-up) code.
Abstract example:
Consider Project 1 with Program.cs:
namespace Project1 {
public class Program {
public static void Main() {
Console.WriteLine("All your Main are belong to us");
Plugin pluginClass = MagicallyGetInstanceOfPluginClassProbablyThroughInjection();
pluginClass.DoSomethingSpecificDependingOnPluginClassDefinition();
}
private Plugin MagicallyGetInstanceOfPluginClassProbablyThroughInjection(){
/*...*/
}
}
public interface Plugin {
void DoSomethingSpecificDependingOnPluginClassDefinition();
}
}
Now consider Project 2 with only class MyPlugin.cs:
namespace Project2 {
using Project1;
public class MyPlugin: Plugin {
public void DoSomethingSpecificDependingOnPluginClassDefinition() {
Console.WriteLine("I'm doing something specific!");
}
}
}
Things to point out:
Project 1 is just a library, possibly nuget'ed
It's Project 2 that imports Project 1, not the other way around
The MyPlugin.cs class above is the only class/file in the project (excluding manifests, app configs, etc)
Aim:
Project 2 should compile into an executable, running Project 1's Main function without writing any more code (no boiler-plate start-up/set-up code). There can then be Project 3, 4, 5, ... that all implement their Plugin-specific code, import Project 1 and run as independent instances.
Is this possible to do? Or do I still have to make a main method in each project that calls the imported project's start-up code? Many thanks in advance!
You could create a plugin container that scans the directory for assemblies and tries to load them. For this you would need a shared interface (interface known to your program and the plugins.
You could then add the DLLs into a defined plugin directory or you could reference the projects inside your main running project.
An example of the interface could be:
public interface IStandAlone
{
void Run();
}
And 1 or to simple implementations could be
public class Program1 : IStandAlone
{
public void Run()
{
Console.WriteLine("Program1");
}
}
public class Program2 : IStandAlone
{
public void Run()
{
Console.WriteLine("Program 2");
}
}
Then you would need to load the possible assemblies, either from the current assemblies (as is done in this example), or by scanning a directory for dlls that might have your type.
An example that scans the current assemblies for any implementations of the a definite type:
public class PluginContainer<T>
{
Type targetType = typeof(T);
public virtual IList<Type> GetMatchingTypes()
{
Assembly[] currentAssemblies = AppDomain.CurrentDomain.GetAssemblies();
IList<Type> items = new List<Type>();
if (currentAssemblies == null || currentAssemblies.Length == 0)
{
Console.WriteLine("No assemblies found!");
return items;
}
foreach (Assembly ass in currentAssemblies)
{
try
{
var types = ass.GetTypes();
foreach (var t in types)
{
if (t.IsInterface)
{
continue;
}
if (!(targetType.IsAssignableFrom(t)))
{
continue;
}
items.Add(t);
}
}
catch (ReflectionTypeLoadException rtle)
{
/* In case the loading failed, scan the types that it was able to load */
Console.WriteLine(rtle.Message);
if (rtle.Types != null)
{
foreach (var t in rtle.Types)
{
if (t.IsInterface)
{
continue;
}
if (!(targetType.IsAssignableFrom(t)))
{
continue;
}
items.Add(t);
}
}
}
catch (Exception ex)
{
/* General exception */
Console.WriteLine(ex.Message);
}
}
return items;
}
public IList<T> GetPlugins()
{
IList<Type> matchingTypes = GetMatchingTypes();
IList<T> items = new List<T>();
if (matchingTypes == null || matchingTypes.Count == 0)
{
Console.WriteLine("No matching types of {0} found", typeof(T).FullName);
return null;
}
foreach (Type type in matchingTypes)
{
try
{
T nObj = (T)Activator.CreateInstance(type);
items.Add(nObj);
}
catch (Exception ex)
{
Console.WriteLine("Error occured trying to run {0}\r\n{1}", type.FullName, ex.Message);
}
}
return items;
}
}
which can then be used inside a main method to scan for any available plugins, and to execute them:
static void Main(string[] args)
{
PluginContainer<IStandAlone> container = new PluginContainer<IStandAlone>();
var plugins = container.GetPlugins();
foreach (var plugin in plugins)
{
plugin.Run();
}
Console.ReadLine();
}
which eventually gives as an output:
Program1
Program 2
Please keep in mind, that this is a very basic example, and that a well thought out interface should be in place, that really only contains the basics, and might give some feedback to the program running the plugins (though this shouldn't be a requirement). Also offering a versions for plugins, maybe an update Url, and such things could be handy in case your plugins can be maintained or implemented by 3th party providers...
I believe the requirement for a start-up method is that it's signature needs to be public static void and it needs to have a single string[] parameter. It may also need to be named "Main", but I doubt it. If a method matches those requirements it should be available to pick as the start-up method in a project's properties.
However the start-up method is what's used to run a stand-alone executable program when it's launched. I believe what you are looking for is more of a plug-in architecture. You can create an attribute and tag your entry point method(s) with that attribute. Then, in your service, you would need to reflect over the classes in the plug-in assembly which you are loading and find methods marked with your custom attribute, and invoke the appropriate one.
Sorry if this sounds a bit vague but a "plugin architecture" is not a trivial topic.
An alternative would be to use the System.Diagnostics.Process.Start(string) method to just launch your "plug-in" as a stand-alone program.
I'm not really sure what you're asking for. Every C# project is either a .exe or a .dll. A .dll does not have a main method, but an .exe needs one. Here's the link describing what it should look like.
If you have many very similar applications then you can move all the common stuff in a .dll project and reference it in all the applications. Then you can call the methods from each .exe. You'll still have a Main() method in each .exe, but it will only contain one line which calls the common implementation.
Or you can do something like a plugin architecture, where you have one .exe and all the other applications are .dll projects, which are loaded and executed by the .exe as needed.
Six of one, half a dozen of the other, it's all the same in the end.

Modules Security in C# Windows Form Applications?

How can i separate my applications to different modules? End users will use only modules that they bought. Can i use that using plug-and-pluy?
You could to take a look into Managed Extensibility Framework to add modules dynamically.
Yes.
You can dynamically load assemblies based on a common interface or markup attributes.
Dynamic assembly loading
You might want to investigate Composite Application Block (CAB) (but it does have a learning curve)
MS has an example and tools for this in their Shareware Starter Kit für C#. Download from here:
http://msdn.microsoft.com/en-us/library/cc533530.aspx
It's somewhat old. But if you are looking for free resources (there are some commercial solutions) this is all that afaik exists without developing the entire infrastructure from scratch.
If you don't want to use MEF, try this.
First of all define (in a library) your interfaces:
namespace Interfaces
{
public interface IPlugin01
{
string Name { get; }
string Description { get; }
void Calc1();
}
public interface IPlugin02
{
void Calc2();
}
}
Then write your plugins (probably assemblies in DLL files) using classes implementing your interfaces (one or more):
namespace Plugin01
{
public class Class1 : Interfaces.IPlugin01,Interfaces.IPlugin02
{
public string Name { get { return "Plugin01.Class1"; } }
public string Description { get { return "Plugin01.Class1 description"; } }
public void Calc1() { Console.WriteLine("sono Plugin01.Class1 Calc1()"); }
public void Calc2() { Console.WriteLine("sono Plugin01.Class1 Calc2()"); }
}
public class Class2 : Interfaces.IPlugin01
{
public string Name { get { return "Plugin01.Class2"; } }
public string Description { get { return "Plugin01.Class2 description"; } }
public void Calc1() { Console.WriteLine("sono Plugin01.Class2 Calc1()"); }
}
}
Finally create your app that loads and uses your plugins:
namespace Test
{
class Program
{
/// ------------------------------------------------------------------------------
/// IMPORTANT:
/// you MUST exclude Interfaces.dll from being copied in Plugins directory,
/// otherwise plugins will use that and they're not recognized as using
/// the same IPlugin interface used in main code.
/// ------------------------------------------------------------------------------
static void Main(string[] args)
{
List<Interfaces.IPlugin01> list1 = new List<Interfaces.IPlugin01>();
List<Interfaces.IPlugin01> list2 = new List<Interfaces.IPlugin01>();
List<Interfaces.IPlugin01> listtot = GetDirectoryPlugins<Interfaces.IPlugin01>(#".\Plugins\");
Console.WriteLine("--- 001 ---");
foreach(Interfaces.IPlugin01 plugin in list1)
plugin.Calc1();
Console.WriteLine("--- 002 ---");
foreach (Interfaces.IPlugin01 plugin in list2)
plugin.Calc1();
Console.WriteLine("--- TOT ---");
foreach (Interfaces.IPlugin01 plugin in listtot)
plugin.Calc1();
Console.ReadLine();
}
public static List<T> GetFilePlugins<T>(string filename)
{
List<T> ret = new List<T>();
if (File.Exists(filename))
{
Assembly ass = Assembly.LoadFrom(filename);
foreach (Type type in ass.GetTypes())
{
if (!type.IsClass || type.IsNotPublic) continue;
if (typeof(T).IsAssignableFrom(type))
{
T plugin = (T)Activator.CreateInstance(type);
ret.Add(plugin);
}
}
}
return ret;
}
public static List<T> GetDirectoryPlugins<T>(string dirname)
{
/// To avoid that plugins use Interfaces.dll in their directory,
/// I delete the file before searching for plugins.
/// Not elegant perhaps, but functional.
string idll = Path.Combine(dirname, "Interfaces.dll");
if (File.Exists(idll)) File.Delete(idll);
List<T> ret = new List<T>();
string[] dlls = Directory.GetFiles(dirname, "*.dll");
foreach (string dll in dlls)
{
List<T> dll_plugins = GetFilePlugins<T>(Path.GetFullPath(dll));
ret.AddRange(dll_plugins);
}
return ret;
}
}
Just a comment: my solutions (containing interfaces, plugins and test console app) compiled my app in .\bin and plugins in .\bin\Plugins. In both folders was deployed Interfaces.dll on which my projects rely on. This is a serious problem, remember (read comments in code) !!!
So you can compile your plugins avoiding that Interfaces.dll is copied in .\bin\Plugins dir; but if you forget this your app won't work at all; so I decided to force dll deletion before searching and loading plugins.

Correct Way to Load Assembly, Find Class and Call Run() Method

Sample console program.
class Program
{
static void Main(string[] args)
{
// ... code to build dll ... not written yet ...
Assembly assembly = Assembly.LoadFile(#"C:\dyn.dll");
// don't know what or how to cast here
// looking for a better way to do next 3 lines
IRunnable r = assembly.CreateInstance("TestRunner");
if (r == null) throw new Exception("broke");
r.Run();
}
}
I want to dynamically build an assembly (.dll), and then load the assembly, instantiate a class, and call the Run() method of that class. Should I try casting the TestRunner class to something? Not sure how the types in one assembly (dynamic code) would know about my types in my (static assembly / shell app). Is it better to just use a few lines of reflection code to call Run() on just an object? What should that code look like?
UPDATE:
William Edmondson - see comment
Use an AppDomain
It is safer and more flexible to load the assembly into its own AppDomain first.
So instead of the answer given previously:
var asm = Assembly.LoadFile(#"C:\myDll.dll");
var type = asm.GetType("TestRunner");
var runnable = Activator.CreateInstance(type) as IRunnable;
if (runnable == null) throw new Exception("broke");
runnable.Run();
I would suggest the following (adapted from this answer to a related question):
var domain = AppDomain.CreateDomain("NewDomainName");
var t = typeof(TypeIWantToLoad);
var runnable = domain.CreateInstanceFromAndUnwrap(#"C:\myDll.dll", t.Name) as IRunnable;
if (runnable == null) throw new Exception("broke");
runnable.Run();
Now you can unload the assembly and have different security settings.
If you want even more flexibility and power for dynamic loading and unloading of assemblies, you should look at the Managed Add-ins Framework (i.e. the System.AddIn namespace). For more information, see this article on Add-ins and Extensibility on MSDN.
If you do not have access to the TestRunner type information in the calling assembly (it sounds like you may not), you can call the method like this:
Assembly assembly = Assembly.LoadFile(#"C:\dyn.dll");
Type type = assembly.GetType("TestRunner");
var obj = Activator.CreateInstance(type);
// Alternately you could get the MethodInfo for the TestRunner.Run method
type.InvokeMember("Run",
BindingFlags.Default | BindingFlags.InvokeMethod,
null,
obj,
null);
If you have access to the IRunnable interface type, you can cast your instance to that (rather than the TestRunner type, which is implemented in the dynamically created or loaded assembly, right?):
Assembly assembly = Assembly.LoadFile(#"C:\dyn.dll");
Type type = assembly.GetType("TestRunner");
IRunnable runnable = Activator.CreateInstance(type) as IRunnable;
if (runnable == null) throw new Exception("broke");
runnable.Run();
I'm doing exactly what you're looking for in my rules engine, which uses CS-Script for dynamically compiling, loading, and running C#. It should be easily translatable into what you're looking for, and I'll give an example. First, the code (stripped-down):
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using CSScriptLibrary;
namespace RulesEngine
{
/// <summary>
/// Make sure <typeparamref name="T"/> is an interface, not just any type of class.
///
/// Should be enforced by the compiler, but just in case it's not, here's your warning.
/// </summary>
/// <typeparam name="T"></typeparam>
public class RulesEngine<T> where T : class
{
public RulesEngine(string rulesScriptFileName, string classToInstantiate)
: this()
{
if (rulesScriptFileName == null) throw new ArgumentNullException("rulesScriptFileName");
if (classToInstantiate == null) throw new ArgumentNullException("classToInstantiate");
if (!File.Exists(rulesScriptFileName))
{
throw new FileNotFoundException("Unable to find rules script", rulesScriptFileName);
}
RulesScriptFileName = rulesScriptFileName;
ClassToInstantiate = classToInstantiate;
LoadRules();
}
public T #Interface;
public string RulesScriptFileName { get; private set; }
public string ClassToInstantiate { get; private set; }
public DateTime RulesLastModified { get; private set; }
private RulesEngine()
{
#Interface = null;
}
private void LoadRules()
{
if (!File.Exists(RulesScriptFileName))
{
throw new FileNotFoundException("Unable to find rules script", RulesScriptFileName);
}
FileInfo file = new FileInfo(RulesScriptFileName);
DateTime lastModified = file.LastWriteTime;
if (lastModified == RulesLastModified)
{
// No need to load the same rules twice.
return;
}
string rulesScript = File.ReadAllText(RulesScriptFileName);
Assembly compiledAssembly = CSScript.LoadCode(rulesScript, null, true);
#Interface = compiledAssembly.CreateInstance(ClassToInstantiate).AlignToInterface<T>();
RulesLastModified = lastModified;
}
}
}
This will take an interface of type T, compile a .cs file into an assembly, instantiate a class of a given type, and align that instantiated class to the T interface. Basically, you just have to make sure the instantiated class implements that interface. I use properties to setup and access everything, like so:
private RulesEngine<IRulesEngine> rulesEngine;
public RulesEngine<IRulesEngine> RulesEngine
{
get
{
if (null == rulesEngine)
{
string rulesPath = Path.Combine(Application.StartupPath, "Rules.cs");
rulesEngine = new RulesEngine<IRulesEngine>(rulesPath, typeof(Rules).FullName);
}
return rulesEngine;
}
}
public IRulesEngine RulesEngineInterface
{
get { return RulesEngine.Interface; }
}
For your example, you want to call Run(), so I'd make an interface that defines the Run() method, like this:
public interface ITestRunner
{
void Run();
}
Then make a class that implements it, like this:
public class TestRunner : ITestRunner
{
public void Run()
{
// implementation goes here
}
}
Change the name of RulesEngine to something like TestHarness, and set your properties:
private TestHarness<ITestRunner> testHarness;
public TestHarness<ITestRunner> TestHarness
{
get
{
if (null == testHarness)
{
string sourcePath = Path.Combine(Application.StartupPath, "TestRunner.cs");
testHarness = new TestHarness<ITestRunner>(sourcePath , typeof(TestRunner).FullName);
}
return testHarness;
}
}
public ITestRunner TestHarnessInterface
{
get { return TestHarness.Interface; }
}
Then, anywhere you want to call it, you can just run:
ITestRunner testRunner = TestHarnessInterface;
if (null != testRunner)
{
testRunner.Run();
}
It would probably work great for a plugin system, but my code as-is is limited to loading and running one file, since all of our rules are in one C# source file. I would think it'd be pretty easy to modify it to just pass in the type/source file for each one you wanted to run, though. You'd just have to move the code from the getter into a method that took those two parameters.
Also, use your IRunnable in place of ITestRunner.
You will need to use reflection to get the type "TestRunner". Use the Assembly.GetType method.
class Program
{
static void Main(string[] args)
{
Assembly assembly = Assembly.LoadFile(#"C:\dyn.dll");
Type type = assembly.GetType("TestRunner");
var obj = (TestRunner)Activator.CreateInstance(type);
obj.Run();
}
}
When you build your assembly, you can call AssemblyBuilder.SetEntryPoint, and then get it back from the Assembly.EntryPoint property to invoke it.
Keep in mind you'll want to use this signature, and note that it doesn't have to be named Main:
static void Run(string[] args)

Categories

Resources