dinsdag 3 april 2012

CSharpCodeProvider + MEF = Match made in heaven

Okay so the following post probably has zero business value (like some other posts do) but I quit enjoyed myself working on it.   What I wanted to try out was make a system that would be extensible by code the user provides… at runtime … in a console application.  So I started fiddling around with the CSharpCodeProvider and was quite impressed by how easy to use it was!
 
So what the CSharpCodeProvider allows you to do is compile code at runtime.  Not something the everyday developer has to do but very fun for hobby projects :-)  So I then thought it would be cool if I could load in that code at runtime and saw the behavior of my code change while I was running it!

Shinny diagram…oooooh

Off course loading code at runtime is a breeze with MEF, so the only thing I had to do is tell MEF in which directory to load up the assemblies.  Just for the sake of putting pretty diagrams on my blog, here is one explaining what I’m trying to do:



Implentation explanation

The idea I came up with was a simple application that responds to the user based on previous expressions that were provided.  Seems pretty abstracting saying it like that so I’ll show you some screenshots to make it more obvious:

So when the user logs in he can either insert a new expression or let the program evaluate userinput.



When the user chooses to Insert an expression he has to provide a lambda statement of the type func<string,bool>.  This statement will later on be used to evaluate user input. 

Then he is supposed to input an answer that the application will provide when the statement returns true.



The application will then inject the lambda and the answer into some source code and compile it to a new assembly.

If we then choose to evaluate user input the application will load all at runtime compiled assemblies and use them to evaluate the users input.




The user can then choose to provide as much expressions and answers as he likes and change the behavior of how the system will respond to certain user input. 

Now the geek part begins… code!

So when the application asks for the expression and answer it will inline the requested input in the following string:

private string CreateSourceCode(string lambda, string answer)
        {
            return
    @"using System;
    using System.ComponentModel.Composition;
    using TestCompilation;
    [Export(typeof(IExpression))]
    class NewExpression : IExpression
    {
        public Func<string, bool> Expression
        {
            get
            {
                return " + lambda + @"
            }
        }

        public string Answer
        {
            get
            {
                return " +
    "\"" + answer + "\"" + @";
            }
        }
    }";
        }

Essentially the user input in combination with the string should provide a valid class in source code.  Maybe you’ve noticed the Export attribute on top of the class?  This is where MEF comes in.  When this source code is compiled we will use MEF to load the compiled assembly at runtime and inject it in our code.  The IExpression interface is just a simple interface with the two properties in it.

But how do we compile this at runtime?  We use the CSharpCodeProvider:

 var location = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
var csc = new CSharpCodeProvider(new Dictionary<stringstring>() { { "CompilerVersion""v4.0" } });
var parameters = new CompilerParameters(new[] { "mscorlib.dll""System.Core.dll""System.ComponentModel.Composition.dll""ConsumerContracts.dll" },
                                                    location + "\\Extensions\\" + Path.GetRandomFileName() + ".dll",
                                                    true);
 
            CompilerResults results = csc.CompileAssemblyFromSource(parameters, sourceCode);

In the CompilerParameters we have to tell the CSharpCodeProvider which assemblies to use to be able to build our class.  If we leave these out it will return the TypeNotFound exception.

The result of the CompileAssemblyFromSource method will be a dll file located in the Extensions folder of my running assembly.
 
So the next step is to load in the assemblies located in the Extensions folder:
 
private void LoadExtensions()
{
    var location = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
    var catalog = new DirectoryCatalog(location + "\\Extensions\\");
    var container = new CompositionContainer(catalog);
    var batch = new CompositionBatch();
    batch.AddPart(this);
    container.Compose(batch);
}

All classes that implement IExtension will then be injected into the following member.

[ImportMany(typeof(IExpression))]
public ICollection<IExpression> Expressions = new List<IExpression>();

The only thing we need to do now is evaluate the users input against the loaded expressions.

private void EvaluateExpression()
{
    Console.WriteLine("Please say something and I will try to answer.");
    var userInput = Console.ReadLine();
    var matchingExpression = Expressions.FirstOrDefault(e => e.Expression(userInput));
    if (matchingExpression == null)
    {
        Console.WriteLine("Could not answer you.");
    }
    else
    {
        Console.WriteLine(matchingExpression.Answer);
    }
    Console.ReadKey();
    Console.Clear();
}


90% of readers won’t read this part

I probably lost 90% of you by now and my feedburner statistics will be dropping to an all-time low but, for the few diehard people that are interested in the full source code, you can find it on github.  Be aware that this was created in an hour or so, so don’t expect the source code to be all that.

Have fun!

Geen opmerkingen:

Een reactie posten