Part 8 - Referencing implementations by key

8_implementations_by_key.png

So far we've looked at registering one implementation for any one service, you could call them the "default" implementation, because it's the implementation the container returns when you ask for that service... but your not limited to only having one implementation registered, and the way to do this is by giving each implementation that's registered a unique key.


There are many reasons for doing this... but in this example we'll look at one reason - because we want to vary the configuration info used...


So here's our component's code:



public class FileReader
{
private string _fileName;

public string FileName
{
get { return _fileName; }
set { _fileName = value; }
}

public string ReadToEnd()
{
return File.ReadAllText(_fileName);
}
}



It's a simple class which lets us read the contents of a file as many times as we like... so we're going to try registering it twice, with different configurations (and different identifiers) in the container.




type="Castle.Windsor.Configuration.AppDomain.CastleSectionHandler, Castle.Windsor" />



file1.txt


file2.txt






So far so good, lets have a look at the app itself - notice that the file1Reader and file2Reader instances are "resolved" with an extra parameter - which is the id, or key, used to select a
particular implementation... but what about the default instance, which reader will that be?


static void Main(string[] args)
{
WindsorContainer container = new WindsorContainer(new XmlInterpreter());

FileReader defaultReader = container.Resolve();
FileReader file1Reader = container.Resolve("reader.file1");
FileReader file2Reader = container.Resolve("reader.file2");

Console.WriteLine("Default contents: {0}", defaultReader.ReadToEnd());
Console.WriteLine("File1 contents: {0}", file1Reader.ReadToEnd());
Console.WriteLine("File2 contents: {0}", file2Reader.ReadToEnd());

Console.Read();
}



And here's the results of running the program:



Default contents: This is the contents of file 1.

File1 contents: This is the contents of file 1.

File2 contents: This is the contents of file 2.




So you can see that file1Reader and defaultReader are the same - and file1Reader was the first FileReader to be registered in the container -and that's the rule, the first one
registered is the default... which is another example of convention over configuration with Castle, rather then having to explicitly denote which is the default with some more xml noise.


Though with a little thinking you can find plenty of ways to access implementations by key to solve certain problems, for the
unimaginative ... how about an application where you can provide a uri to send plain text messages to ... and depending on the scheme
you need to find an implementation that can do the work of sending the message.



You have a bunch of destinations, addressed by Uri...
  • file://c:/temp/log.txt
  • ftp://ftp.google.com/log.txt
  • http://www.bittercoder.com/SimpleMessageService.aspx
  • fax://64215559555


And then you can just register the implementations/schemes you wish to support...
  • messageSender.file
  • messageSender.ftp
  • messageSender.http
  • messageSender.fax

And then look them up as required, using something like


Uri uri;
// blah blah blah
string key = "messageSender."+ uri.Scheme;
if (container.Kernel.HasComponent(key))
{
ISender sender = container.Resolve(key);
// send the message at this point...
sender.SendMessage("hi there");
}
else
{
// no implementation registered...
throw new NotImplementedException("no sender registered for scheme: " + uri.Scheme);
}


Personally I wouldn't do this ;o) but there's certainly no technical reason why you couldn't... notice how we check that a
component is registered for that key - we could just let the container throw an exception... but then we wouldn't know how to
check that a container has a certain key registered in it, would we!



The next part will be on constructor injection... and following that setter injection.
Written on April 17, 2007