I can only describe what we have come up with. We've borrowed usability syntax and such from various online libraries, but the code is all ours.
Basically, we have what we call a ServiceContainer, an object. There is always a global instance of it, a singleton copy if you wish, static, and thus in a web-application, shared between all users in the appdomain.
A ServiceContainer contains rules. The rules says things like If someone asks for an object of type XYZ, here's how you go about providing them with it.
For instance, a rule might be that in order for some code to get an object that implements IDbConnection, the container would construct, configure, and return a new SqlConnection object.
The code in question would thus not know, and not care, that it is using a SqlConnection object, as opposed to a OleDbConnection object.
Having written that, I realize this is not a very good example, because ultimately you end up asking the connection for command objects, and the SQL syntax you give to that object must be tailored to the type of connection. But if we can disregard that point right now, the code would not know that it is connecting to SQL Server, it just knows that it has a connection object.
Now, the code in question here would need to be given the container it should use, and thus the rules. This means that from a unit-testing perspective, I could create a new instance of ServiceContainer, write new rules into it for testing purposes, and ask the code to do its thing. Ultimately the code would want to execute some SQl (in this instance) and instead of talking to a real database, it would call my test implementation of IDbConnection and IDbCommand, and thus give me an opportunity to verify that things are working.
More importantly, it gives me a way to return back known dummy-data fitting the test, without having to mock up an entire database.
Now, for the injection part, in our case we can ask the container to provide us with objects, that has to be constructed, that relies on other objects.
For instance, let's say we have a IDataAccessLayer interface, and a MSSQLDataAccessLayer implementation.
While the interface doesn't give us any outwards sign that it does any logging, the actual implementation here needs to have somewhere to log all the SQL it executes. Thus, the constructor for the class might look like this:
public MSSQLDataAccessLayer(ILogger logger) { ... }
In the ServiceContainer object, we have registered the following rules (this is our syntax, you won't find that anywhere else, but it should be easy enough to follow):
ServiceContainer.Global.RegisterFactory<ILogger, FileLogger>()
.FactoryScoped()
.WithParameters(
new Parameter("directory", @"C:\Temp")
);
ServiceContainer.Global.RegisterFactory<IDataAccessLayer, MSSQLDataAccessLayer>()
.FactoryScoped();
FactoryScoped means that each time I ask the container for an object, I get a new one.
The rules, if I write them in english, are like this:
- If anyone needs an implementation if ILogger, construct a new FileLogger object, and take the constructor that needs a "directory" parameter, and use that constructor while passing in "C:\Temp" as the argument
- If anyone needs an implementation of IDataAccessLayer, construct a new MSSQLDataAccessLayer
Notice that I said before that the constructor to MSSQLDataAccessLayer takes an ILogger, yet I didn't specify any parameters here? This gives me the following code to get hold of the access layer object:
IDataAccessLayer dal = ServiceContainer.Global.Resolve<IDataAccessLayer>();
What happens now is that the container object figures out that there the object is MSSQLDataAccessLayer, and that it has a constructor. This constructor requires an ILogger object, but lo and behold, the container knows how to make one. The container will thus construct a new FileLogger object, and pass this to the constructor of MSSQLDataAccessLayer object, silently.
Configuration of much of the application dependencies can thus be done once, somewhere central and executed during startup, while the rest of the code is blissfully unaware of all the magic happening here.
For unit-testing purposes I can rewrite the rules to provide my own dummy logger object that just stores the logged text in memory, which allows me to easily verify that what I expected the code to log was actually logged, without having to read in a file afterwards.
The rules gives us lots of power on how to actually provide object instances:
- From a delegate/method, which means we can do all the magic of constructing dependent objects ourselves if we want to
- Automatically from a constructor (either automatically figured out which one to use, or we can provide enough dummy-parameters by names/types to pick one)
- Or we can provide an existing instance to the container (this will then sort-of be like a singleton)
We looked at autofac before coming up with our own, basically we just looked at the wiki showing examples of call syntax and then sat down and wrote our own system that did what we needed.