Endjin - Home

Creating your own (simple) dependency injection framework: Service Locator

by Jonathan Donaldson

dependency injection framework

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.

Service Locator

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.

  • The IPersonRepository is being registered to the concrete type of SqlRepository.
  • Asking the container to resolve the IPersonRepository, it will return with a concrete type of SqlRepository, as shown in the output of the Console.WriteLine.

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.

  • The TypeDict is used to store the mapping between type to resolve and the concrete type.
  • When the Register method is called the type to resolve is used as the key and the value of the TypeDict is the concrete type.
  • When the Resolve method is called it simply finds the type to resolve in the TypeDict and uses the Activator class to create a new instance of the concrete type.
  • The where clauses on the methods are used to constrain the generic types so only reference types e.g. classes or interfaces can be passed in.

Lifecycle Management

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:

  • ReferenceEquals(resolvedSingleton1, resolvedSingleton2) returns True as with the singleton lifecycle only one instance of the concrete type is used when the Resolve method is called.
  • ReferenceEquals(resolvedTransisent1, resolvedTransisent2) returns False as with the transient lifecycle an instance of the concrete type is created each time the Resolve method is called.

Summary

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:

Be sure to check out part 2 where I will be implementing constructor injection, a proper DI pattern.

About the author

Jonathan is a 1st year Apprentice Software Engineer, having recently gained a Computer Science degree from the University of Lincoln, he is just starting his journey in solving technology problems for clients. You can follow Jonathan on Twitter.