When I am learning something new and it’s programming related I normally find the best way to learn is to write my own implementation. I have been reading a lot about dependency injection so I thought it would be worthwhile to create my own DI framework so I could see how it actually works.
I am still learning about C# and DI so there will be things wrong with my implementation and there are definitely better ways to do everything. The point of this exercise is to see how DI containers work and not to create the next best DI framework, for that you will have to look at Matthew’s and Mike’s blogs.
One of the resources I had available while learning about dependency injection was the excellent blog post by Alice. I highly recommend that you read it if you haven’t come across dependency injection before or want to know why using a DI framework is really useful.
Life before dependency injection
Before I learned about dependency injection my world was a simpler place, when I wanted a new instance of an object, I just did this:
I used the
new keyword and I got what I asked for, a
new instance of the
FileRepository class. This may seem harmless in smaller applications, but it makes code difficult to maintain and extend.
What if the constructor signature changes?
I would have to update the constructor to deal with this change but It might require more than just a localised change. If I didn’t have all the things the new constructor required locally, I would have to change the signature of the method that calls the constructor, and similar changes would ripple up through the call tree. This could lead to me making errors when the updating code and result in the code being less maintainable.
What if I wanted to test the code that uses the FileRepository?
The code is harder to unit test, as without being able to easily change the
FileRepository to a mock, I would have to spend my time changing it.
Without changing it to a mock then I would be testing the code of the
FileRepository as well as the code that uses it, which no longer makes it a unit test.
Instead of going straight to dependency injection, I’m going to start with a simpler way of solving the above problems: a service locator.
It’s considered to be an anti-pattern, so we’ll be moving away from this, but it helps to use the service locator first so we can see its shortcomings and understand why dependency injection is better.
The service locator (anti-)pattern is straightforward: it provides a method you can call, where you state the interface you want and it returns an implementation of that interface.
I started out by creating what a service locator needs:
- A way of registering the concrete type that will implement a particular interface
- A way to get object that implements a particular interface
This example calls the SimpleContainer’s Register method and the types to register are passed in via the generic type parameters.
IPersonRepositoryis being registered to the concrete type of
- Asking the container to resolve the
IPersonRepository, it will return with a concrete type of
SqlRepository, as shown in the output of the
In the real world, the register and resolve methods would never be next to each other like this. The entire point of this pattern is that these two calls happen in quite different parts of the code base, meaning that code that uses that interface doesn’t need to know what type implements it.
But how does the register method work and how are the types resolved?
If you haven’t come across C# generics before this code might scare you, but don’t worry it’s rather simple.
TypeDictis used to store the mapping between type to resolve and the concrete type.
- When the
Registermethod is called the type to resolve is used as the key and the value of the
TypeDictis the concrete type.
- When the
Resolvemethod is called it simply finds the type to resolve in the
TypeDictand uses the
Activatorclass to create a new instance of the concrete type.
whereclauses on the methods are used to constrain the generic types so only reference types e.g. classes or interfaces can be passed in.
Another feature of a DI framework is lifecycle management. This is useful if we want just one instance of a concrete type across our application (a singleton) or for a new instance to be created for every resolve request (transient). There are other lifecycles, but I will only be implementing these two to keep things simple.
To implement lifecycles, I added the following to my code:
- A dictionary to store the instances of the concrete type.
- An enum to tell what lifecycle should be used.
- An overloaded Register method to take in the enum.
Which resulted in the following code:
and means that a concrete type can be registered with a lifecycle using the following code:
Trueas with the singleton lifecycle only one instance of the concrete type is used when the
Resolvemethod is called.
Falseas with the transient lifecycle an instance of the concrete type is created each time the
Resolvemethod is called.
What has the service locator achieved?
It can achieve nearly everything that a proper DI framework can:
- We achieve testability as we can swap in concrete types for mocks.
- We achieve good separation of concerns, so we can write maintainable code.
- If we extended this example we could achieve late binding.
Problems with the service locator
As I have mentioned before the service locator is an antipattern as:
- The code is now dependant on the service locator.
- Service Locator violates SOLID.
- Service Locator violates encapsulation.
Be sure to check out part 2 where I will be implementing constructor injection, a proper DI pattern.