This post is about creating my own dependency injection system.
Dependency injection allows us to inject dependencies at run-time instead of compile-time. I’m going to start with the simple version, which I’ll describe using the following requirements:
- I want to define a binding between an interface and a class using generics. Obviously the class must implement the interface.
- This means that all dependencies must be known at compile-time. So for now I’m not going to include any run-time loaded types based on string values, or any other similar methods.
- I want to use fluent interfaces to define the binding.
- I want to be able to resolve an instance of an object by specifying the interface type.
Going from the list above I could come up with the following way to define a binding and to get an instance of an class:
1: namespace MyDI_Tests {
2: public static class Test {
3: public static void DoTest() {
4: Factory.Bind<ITestObject>().To<TestObject>();
5: var test = Factory.GetInstance<ITestObject>();
6: // test should be of type TestObject
7: }
8: }
9: }
In the text below, I will reference to the ITestObject type as the interface-type and the TestObject type as the class-type.
All right, lets make it happen. First we need a Factory class.
1: namespace MyDI {
2: public static class Factory {
3: public static IBindingRule<TInterface> Bind<TInterface>() {
4: // TODO
5: throw new NotImplementedException();
6: }
7:
8: public static TInterface GetInstance<TInterface>() {
9: // TODO
10: throw new NotImplementedException();
11: }
12: }
13: }
We are also in need of an interface to define a binding rule. We’ll worry about the implementation later.
1: namespace MyDI {
2: public interface IBindingRule<TInterface> {
3: void To<TClass>() where TClass : class, TInterface, new();
4: }
5: }
And presto, now we can define binding rules in the factory. Let’s create a unittest. We need a dummy interface and class, and a unittest class.
1: namespace MyDI_Tests {
2: interface ICommunicator {
3: void SendCommand(string command);
4: }
5:
6: class WirelessCommunicator : ICommunicator {
7: public void SendCommand(string command) {
8: throw new System.NotImplementedException();
9: }
10: }
11: }
1: namespace MyDI_Tests {
2: [TestClass]
3: public class FactoryTests {
4: [TestMethod]
5: public void TestMethod1() {
6: Factory.Bind<ICommunicator>().To<WirelessCommunicator>();
7: var test = Factory.GetInstance<ICommunicator>();
8: Assert.IsNotNull(test);
9: Assert.AreEqual(typeof(WirelessCommunicator), test.GetType());
10: }
11: }
12: }
Now we can run our unittest, and obviously it fails.
We need to create a binding rule class, and finish the factory class. To do that, we are also going to need another class, a class which will actually create an instance of an object. The reason for this is the following: When the Factory.Bind<TInterface>() method is called, an instance of the binding rule class is created. But the IBindingRule<TInterface>.To<TClass>() method specifies to which class-type the binding-rule should use. Therefore the binding rule cannot hold the class-type, and this is where the ‘instance creator’ comes into play. The instance creator class knows the interface-type, and the class-type, and therefore can return an object of the class-type.
I was struggling with the instance creator class at first, but once I figured out that the interface doesn’t know the class-type, and the class does know the class-type, things started to work out.
To reference the non-generic binding rules from the Factory a non-generic binding rule interface is necessary; the generic binding rule interface of course inherits this interface as well.
Lets create the binding rule class, the instance creator interface and class, and finish the factory class.
1: namespace MyDI {
2: interface IInstanceCreator<TInterface> {
3: TInterface Create();
4: }
5: }
1: namespace MyDI {
2: class InstanceCreator<TInterface, TClass> : IInstanceCreator<TInterface>
3: where TClass : class, TInterface, new() {
4: public TInterface Create() {
5: return new TClass();
6: }
7: }
8: }
1: namespace MyDI {
2: public interface IBindingRule { }
3:
4: public interface IBindingRule<TInterface> : IBindingRule {
5: void To<TClass>() where TClass : class, TInterface, new();
6: }
7: }
1: namespace MyDI {
2: class BindingRule<TInterface> : IBindingRule<TInterface> {
3: private IInstanceCreator<TInterface> _creator;
4:
5: public void To<TClass>() where TClass : class, TInterface, new() {
6: _creator = new InstanceCreator<TInterface, TClass>();
7: }
8:
9: public TInterface Create() {
10: if (_creator != null) {
11: return _creator.Create();
12: }
13: throw new InvalidOperationException();
14: }
15: }
16: }
1: namespace MyDI {
2: public static class Factory {
3: public static IBindingRule<TInterface> Bind<TInterface>() {
4: throw new KeyNotFoundException();
5: if (!_bindingRules.ContainsKey(typeof(TInterface))) {
6: var bindingRule = new BindingRule<TInterface>();
7: _bindingRules.Add(typeof(TInterface), bindingRule);
8: return bindingRule;
9: }
10: throw new InvalidOperationException();
11: }
12:
13: private static readonly Dictionary<Type, IBindingRule> _bindingRules = new Dictionary<Type, IBindingRule>();
14:
15: public static TInterface GetInstance<TInterface>() {
16: if (_bindingRules.ContainsKey(typeof(TInterface)) && _bindingRules[typeof(TInterface)] is BindingRule<TInterface>) {
17: return ((BindingRule<TInterface>)_bindingRules[typeof(TInterface)]).Create();
18: }
19: throw new InvalidOperationException();
20: }
21: }
22: }
So what is exactly happening here?
Interface IInstanceCreator<TInterface> This interface defines an instance creator. Generics-wise it only knows the interface-type, since the binding rule must hold a reference to the instance creator. It has a single method, the Create() method, which is meant to return an instance of an object which implements the interface-type.
Class InstanceCreator<TInterface, TClass> This class implements the IInstanceCreator<TInterface> interface, and therefore has a Create() method. This method can simple create a new object of the class-type using the parameterless constructor.
Interface IBindingRule This interface defines a non-generic binding rule. This is necessary for the factory to reference a collection of binding rules. Of course it could be a list of objects as well, but using this interface it is somewhat strongly typed.
Interface IBindingRule<TInterface> This interface defines a generic binding rule. This interface exposes a single method to define the binding, specifying the class-type using generics.
Class BindingRule<TInterface> This class implements the IBindingRule<TInterface> interface. It is actually very simple. When the To<TClass> method is called, an instance of the InstanceCreator class is created. When the Create method is called, the instance creator is used to create an instance of the class-type.
Class Factory This class provides public access to the binding rules by exposing two static methods. One is the Bind<TInterface> method which can be used to create a binding rule. The binding rule is stored in a private dictionary. The other method is the GetInstance<TInterface> method in which a binding rule is retrieved from the dictionary, and the Create-method is used to get an instance of the object.
Now we have a unittest that passes. Excellent!
Using the factory we can define a binding rule in one assembly, and get an instance of a class that normally wouldn’t be accessible (without using reflection or some other method). The following diagram shows what I mean.
In the core assembly an interface is defined, and in that same assembly we want to use it. But the implementation is done in the implementing assembly which we can´t reference, since that assembly references the core assembly. This is possible using the factory.
Please note that the code shown above is not yet thread-safe.
In the next post I will introduce the concept of the binding scope. With the binding scope we can specify if and how previously created objects are re-used. For example: a singleton-scope.
The code can be downloaded here.
Great article
ReplyDeleteWe can also submit our .net related article links on http://www.dotnettechy.com to improve traffic.
This website also have directory submission for .net related website / blog only
Hi Micle,
DeleteThank you for your reply.
I have no problem with a link to my article being submitted on your website. Can I do this myself, or do you have to do it?
Kind regards,
Maarten