In the first entry in this series I outlined why it might be better to write your own service container rather than use an off-the-shelf one, and wrote the configuration classes and shell for our initial version of a service container that can load a hierarchy of dependent services. In this entry I'll fill in the LoadServiceHierarchy method to actually perform the loading, and implement a container that can load the services from serialized XML.
Just for fun I'll be using Linq with the C# 3.0 language extensions rather than the usual for/foreach loops (although I do think the new way is a much better programming paradigm as it allows you to express your intention as opposed to expressing the steps you use to achieve your result). Here's the completed method:
private object LoadServiceHierarchy(string name, Stack<string> constructing)
{
object service;
if (this.singletons.ContainsKey(name))
{
service = this.singletons[name];
}
else if (!this.config.Contains(name))
{
throw new InvalidOperationException(string.Format(
"Service '{0}' was not found in hierarchy: {1}.",
name,
GetPathToService(name, constructing)));
}
else if (constructing.Contains(name))
{
throw new InvalidOperationException(string.Format(
"A circular reference was found in hierarchy: {0}.",
GetPathToService(name, constructing)));
}
else
{
constructing.Push(name);
Service serviceConfig = this.config[name];
var constructorArgs =
from arg in serviceConfig.Args
orderby arg.Index
select this.LoadServiceHierarchy(arg.ServiceRef, constructing);
Type serviceType = Type.GetType(serviceConfig.Type, true, true);
service = InstantiateServiceFromType(serviceType, constructorArgs.ToArray());
this.singletons.Add(name, service);
constructing.Pop();
}
return service;
}
First we check whether there is already a singleton instance of the service and use that if it is already constructed. The next couple of checks ensure that we have got configuration information for a service of the specified name, and that the service is not already under construction - these use a helper method GetPathToService to display the hierarchy in human readable form, which I'll expand below.
Finally we construct the service itself so the name is pushed onto the stack of services under construction, and a Linq query is used to recursively instantiate each service in the constructor arguments configured for the service, which will give the implicitly typed variable constructorArgs a type of IEnumerable<object>. The service is instantiated using another helper method InstantiateServiceFromType, added to the list of singletons, and then the name popped from the stack of services under construction, at which point we could assert that the popped name is the same as the name argument.
The helper methods are below, and although they are both fairly trivial they show how pervasive the use of Linq methods can be as a means to shorten and clarify your code (try writing the equivalent code using loops and you'll notice it's around twice as long and somewhat less clear as a result). Note that in GetPathToService the stack is reversed to display the hierarchy starting at the root.
private static string GetPathToService(string name, Stack<string> constructing)
{
if (constructing.Count == 0)
{
return name;
}
else
{
string[] serviceNames = constructing.Reverse().ToArray();
return string.Join(" -> ", serviceNames) + " -> " + name;
}
}
private static object InstantiateServiceFromType(Type serviceType, object[] args)
{
Type[] argTypes = (from arg in args select arg.GetType()).ToArray();
ConstructorInfo constructor = serviceType.GetConstructor(argTypes);
if (constructor == null)
{
string[] typeNames = (from t in argTypes select t.FullName).ToArray();
throw new InvalidOperationException(string.Format(
"No public constructor on type {0} matches signature ({1}).",
serviceType.FullName,
string.Join(", ", typeNames)));
}
return constructor.Invoke(args);
}
That's the basic container finished, so to let us use it here's a concrete implementation that deserializes an XML file into a collection of services and then passes it to the base class constructor. I was being lazy here and could have spent more time using the System.Configuration classes to create a proper XML configuration framework for them, but this demonstates the point sufficiently.
public sealed class XmlServiceContainer : RuntimeServiceContainer
{
public XmlServiceContainer(string path)
: base(Deserialize(path))
{
}
private static ServiceCollection Deserialize(string path)
{
XmlSerializer serializer = new XmlSerializer(typeof(ServiceCollection));
using (Stream stream = File.OpenRead(path))
{
return (ServiceCollection)serializer.Deserialize(stream);
}
}
}
At this point we've written a little over a hundred lines of code and we've already got a fully working service container that can create complex hierarchies, detect circular references, and load its configuration information from arbitrary sources. A lot of the time you won't need much else, but sometimes you need to do more complex things to construct a service like pass in a list or a dictionary of other services as a parameter, so next time we'll extend the container to be more flexible with regards to parameter configuration.
Posted
Dec 27 2007, 11:01 AM
by
Greg Beech