June 5, 2021

C# Dependency Injection done right

If you find yourself doing this in your code then it’s something you need to learn!

What is Dependency Injection?

I like to look at electronics and model them into classes in software.

What is a dependency?

A dependency is something a unit of work relies on.
For example a Blender is dependent on its knife - Without a knife it wouldn’t function.

Some example also include:

What is an injection?

Putting a unit of work or data into other unit of work.

A blender is actually an excellent example:

It gives us a way to put a kind of blade into a socket - by the type of the knife we change the functionality of a blender.

Code example

I’ll use interfaces in C# to demonstrate the usage of injection and I’ll use Property injection.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public interface ICutter
{
IEnumerable<TCut> Cut<T, TCut>(T toCut);
}

public class Blender
{
public ICutter CurrentCutter { get; set; }

public void Blend<T,TPiece>(T toBlend)
{
var pieces = CurrentCutter?.Cut<T, TPiece>(toBlend);
/// Do something with pieces
}
}

Types of Code Injections

There are 2 main types of injection:

These types of injection support the Strategy Pattern.
Which you can read here:

Strategy pattern

Decoupling

The main advantage of DI is the decoupling of unit of works into independent units.
So in our example a Blender is decoupled from the knife - the knife is not built in.
Meaning it’s easier to replace, maintain clean and to fix the knife.
Just like code!

Modular units are easier to maintain and fix - if the cutting isn’t right we know which unit does the bad behavior.
As I’ve written in the Strategy Pattern Here it’s also easier to test smaller units.

Dependency Containers

An important concept for DI are IoC (Inversion of Control) containers.
We eliminate the usage of the new operator.

What is a DI container

A container is a unit that stores object types and how to “implement” them.
A container’s job is to create instances of classes.

Usual interface of a container

I’ve implemented already a good container with registration and lifecycle, here’s how an interface looks like:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public enum InstanceLifecycle
{
Instance,
Singelton,
Pooled
}

public interface IDIContainer
{
IDIContainer Register<T>(InstanceLifecycle lifecycle);
IDIContainer Register(Type type, InstanceLifecycle lifecycle);

T Create<T>();
object Create(Type type);
}

The lifecycle of a dependency is:

ImageSaver consists of ICompressor and IStore.
An instance of IStore (Implementation) is dependent on IFileIO.

Registration usually works on a main level of abstraction:

1
Container.Register<IStore, FileStore>();

Then a resolve is made:

1
2
// Store doesn't know the underlying type!  
var store = Container.Resolve<IStore>();

The conatiner can implement it in many ways.
store instance may be a singelton, pooled, new instance, etc…

Named instances

What if we have 2 implementations?

1
2
3
4
5
6
7
8
9
IDIContainer RegisterNamed<TInstance,TImp>(string name);
TInstance Resolve<TInstance>(string name);

// ...
RegisterNamed<IStore, FileStore>("ProductsStore");
RegisterNamed<IStore, FileStore>("ClientsStore");

var productsStore = ResolveNamed<IStore>("ProductsStore");
var clientsStore = ResolveNamed<IStore>("ClientsStore");

Named registration can resolve this conflict.
We can make our code fancier by introducing Attributes and Meta-Data code.

Containers in the wild

I’ll give 3 containers as examples:

StructureMap

Ninject

Unity

Dependency Containers are awesome

In the end using DI and IoC(Inversion of Control) containers give us full power of instantiation of classes.

The only new we used is on the container, everything else is done by the container.
Some containers are smart and can resolve unregistered instances if they know how to construct them:

1
2
3
4
5
6
7
8
9
10
11
public void Main()
{
Container cotainer = new Container();
container.Register<IStore,FileStore>();
container.Register<INetworkParser, NetworkParser>();
container.Register<ICompressor, JpegCompressor>();

// StorageServer isn't even registered!
var server = container.Resolve<StorageServer>();
server.Run();
}

This gives us access to maintaining lifecycle and modularity very easily, I like it.

Thanks for reading!

על הפוסט

הפוסט נכתב על ידי Ilya, רישיון על ידי CC BY-NC-ND 4.0.

שתפו את הפוסט

Email Facebook Linkedin Print

קנו לי קפה

#C##Software