Code Voyeur
RSS
Data Access Languages MVC ORM About Roadmap Contact Site Map RSS Sample Code Presentations Snippets dll Hell .net Rate My Snippet

A Simple IronPython Dependency Injection Framework

Several patterns exist that essentially perform the same function – wire up a graph of objects outside of the code using those objects. Whether using a framework that supports the provider model, dependency injection or full-blown inversion of control, contained objects are often defined in XML.

Consider the following Spring.NET object definition:

<object id="exampleObject" type="Examples.MixedIocObject, ExamplesLibrary">
    <constructor-arg name="objectOne" ref="anotherExampleObject"/>
    <property name="objectTwo" ref="yetAnotherObject"/>
    <property name="IntegerProperty" value="1"/>
</object>

<object id="anotherExampleObject" type="Examples.AnotherObject, ExamplesLibrary"/>
<object id="yetAnotherObject" type="Examples.YetAnotherObject, ExamplesLibrary"/>
An XML schema has been defined to support object definition and setter and constructor injection. The XML even supports mapping constructor arguments and properties to objects defined elsewhere in the XML (ref="...").

While the XML is both easy to work with and easy to follow, it's still XML. Objects within the XML are created without parentheses and properties are set without a dot. A desire for familiar object coding constructs leads some to wire up objects explicitly in application code (most containers support this feature). This approach is more natural, but the benefits of external object configuration are lost (centralized object configuration, behavior changes without builds, etc.). This article isn't advocating one approach over the other, but rather suggesting a middle-of-the road alternative.

Using a simplified XML schema and IronPython, it is possible to construct a simple dependency injection framework. XML will be used for a convenient and queriable configuration store. IronPython will provide an extensible and natural language for wiring an object graph.

The XML is simply a collection of scripts with names.

<objects>
  <object name="AlbumLister" static="true">
...
  </object>
</objects/>
Each object node will contain an object to be configured. The “name” attribute is used as the key to obtaining an object from the container. The “static” attribute tells the framework whether to cache instances of the object or return a new instance with each object request. That is the complete schema definition for the container's XML.

Container implementations will extend the ContainerBase class.

public abstract class ContainerBase
{
    protected static Dictionary<string, object> _StaticObjects = new Dictionary<string, object>();        

    public abstract T GetObject<T>(string objectName);        
    public abstract object GetObject(string objectName);
    
    protected object ConstructObject(ContainedObject co)
    {            
        ...
    }        
}
ContainerBase defines a protected dictionary of objects, _StaticObjects, that holds references to all static container objects. Two abstract GetObject methods are defined. One uses generics and the other returns objects. Support for generics has not been implemented for this article, but is planned. A partial generics solution is available in the revision history for the sample project source.

The protected ConstructObject performs most of the work for the container. It takes a ContainedObject (a simple POCO representing an object's configuration node) as input. It's job is then to execute the script representing the objects configuration and return an wired object instance. The function first checks the _StaticObjects dictionary to see whether an instance is cached and ready to be returned.

protected object ConstructObject(ContainedObject co)
{
    if (_StaticObjects.ContainsKey(co.Name))
    {
        return _StaticObjects[co.Name];
    }            
    ...
}
If no instance is available in the static cache, a PythonEngine instance is created and some default imports are executed to make the project's Model classes available to the scripts. Clearly, this code could be made more efficient by caching the engine or even creating all objects in a single pass. However, those optimizations are left for a future version.
else
{
    PythonEngine engine = new PythonEngine();
    engine.LoadAssembly(Assembly.GetExecutingAssembly());
    engine.Execute("from IronPythonInversionOfControl import *");
    engine.Execute("from IronPythonInversionOfControl.Model import *");
    ...
}
The next sections of ConstructObject are the most important.
engine.Globals["instance"] = null;
engine.Globals["reference"] = ...
engine.Execute(co.Script);
engine.Shutdown();

if (co.IsStatic)
    return _StaticObjects[co.Name] = engine.Globals["instance"];
else
    return engine.Globals["instance"];
Two global variables, "instance" and "reference" are added to the engine. The "instance" variable is meant to hold the configured object. The reference variable will be discussed shortly. Next, the engine is executed and then shutdown. Again, optimizations are left for a later phase. Finally, the instance is returned and optionally added to the _StaticObjects dictionary.
engine.Globals["reference"] = new Func<string, object>
    (
        delegate(string refName)
        {
            if (_StaticObjects.ContainsKey(refName))
                return _StaticObjects[refName];
            else
                return GetObject(refName);
        }
    );
As can be seen above, the global "reference" is actually being assigned a delegate that expects a string and returns an object. Essentially, this delegate is going to check the static objects and return the static instance or pass through GetObject to execute the steps that have been described in the past several paragraphs.

Having seen the code that will process the scripts, it's now time to analyze the scripts themselves. The XML below contains three object definitions, and object wiring.

<objects>
  <object name="AlbumLister" static="true">
instance = AlbumLister()
instance.Finder = reference("AlbumFinder")
instance.Logger = reference("Logger")
  </object>
  <object name="AlbumFinder" static="true">
instance = XmlAlbumFinder("Albums.xml")
instance.Logger = reference("Logger")
  </object>
  <object name="Logger" static="false">
instance = Logger()    
  </object>  
</objects>
Looking from bottom to top, the Logger object is the simplest example of an object configured for the container. The global "instance" variable has been set a new Logger instance. The return value of ConstructObject will be a reference to this new object. It's not static, so it will be recreated for each GetObject request.

The AlbumFinder object sets "instance" to a new XmlAlbumFinder instance. This is a static object, so if it has already been added to the object cache, the script won't be re-executed. This script demonstrates both constructor and setter injection in the IronPython container. Both types of injection amount simply to writing object code the way it's always done – "new" up an object and set its property values.

The other important piece in the AlbumFinder configuration script is the reference("Logger") code. The global variable "reference" is the delegate that was shown above. When the script executes and reference is called, that delegate is in turn executed. The "reference" call returns either a cached or new instance of a container object. The container essentially passed a utility function to the script to allow a contained-object graph to be constructed quite easily.

There's not much more to the container. Basically, IronPython scripts create object instances, set property values and assign those instances back out through global variables. The XmlContainer simply uses LINQ to parse out the script for a given object name and calls the ContainerBase's proteced methods to do the real work.

public override object GetObject(string objectName)
{
    XDocument doc = XDocument.Load(_filePath);

    var objects = from obj in doc.Descendants("object")
                  where obj.Attribute("name").Value == objectName
                  select new ContainedObject
                  {
                      Name = objectName,
                      Script = obj.Value.Trim(),
                      IsStatic = bool.Parse(obj.Attribute("static").Value)
                  };

    foreach (var o in objects)
    {
        return ConstructObject(o);                
    }

    throw new ApplicationException("No object found for given name.");
}
The included sample is a minor variation on Martin Fowler's standard MovieLister sample, substituting albums for movies. Basic usage of the container in the program's Main method is as follows:
ContainerBase container = new XmlContainer("Objects.xml");
AlbumLister lister = container.GetObject("AlbumLister") as AlbumLister;
An instance of the XmlContainer is created, passing in the name of the object definition file to the constructor. The next line gets an instance of an AlbumLister from the container. The XML a few paragraphs above shows the definition and script for the AlbumLister instance.

Finally, the sample project is in no way meant to be considered a fully featured IoC container. Issues like thread safety, recursive reference protection, generics support and better caching are missing. The XML should also be enhanced to allow for "imports" or "assemblies" sections.

Download Sample Project

References

A place to post comments on this article until I have time to add a comments feature to Code Voyeur...
Sample project source code
Article Posted: Wednesday, April 16, 2008

Leave a Comment

Shout it

Contact Code Voyeur about this article.