Dependency Injection in .NET

What is dependency injection?

Dependency injection is a programming technique that makes a class independent of its dependencies. It achieves that by decoupling the usage of an object from its creation. This helps you to follow SOLID’s dependency inversion and single responsiblity principles.

Benefits of Loose Coupling

  • Easy to extend
  • Easy to test
  • Easy to maintain
  • Facilitates parallel development (rare conflict)
  • Facilitates late binding (runtime data binding)

Dependency Injection Patterns

  • Constructor Injection
  • Property Injection
  • Method Injection
  • Ambient Context
  • Service Locator

Application Overview

The application contains four layers

  1. View (UI elements) such as the buttons and the list box
  2. Presentation (UI logic): functions that the buttons call and the property that the data bound to the list box in the UI.
  3. Data Access: Code that knows how to interact with the data store. It knows how to make a web service call, and then translate the results into objects that the Presentation layer can use.
  4. Data Store: where we get the actual data, in this case, the web service.

Tight Coupled Code

In the initial code, all four layers are tightly coupled.

In the view layer, it creates PeopleViewModel()

1
2
3
4
5
6
public PeopleViewerWindow()
{
InitializeComponent();
viewModel = new PeopleViewModel();
this.DataContext = viewModel;
}

In the Presentation layer, it creates serviceReader()

1
2
3
4
public PeopleViewModel()
{
DataReader = new ServiceReader();
}

In the Data Access layer, it hardcoded the web service url

1
2
3
4
5
public class ServiceReader
{
WebClient client = new WebClient();
string baseUri = "http://localhost:9874/api/people";
}

Potential problems

  1. Hard to create unit test.
    If I want to test UI element (like a button), I have to run the web services because they are tightly coupled. (PeopleViewerWindow needs PeopleViewModel which needs ServiceReader which needs WebClient).

  2. Hard to extend
    If I want to add another Data Store like read data from a CSV file or SQL DB, and I also want to have the option to choose to use cached data store. Then in the PeopleViewModel I need to write something like this

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public PeopleViewModel() {
switch(dataReaderType) {
case 'service': DataReader = new ServiceReader();
break;
case 'service_cached': DataReader = new CachedServiceReader();
break;
case 'text': DataReader = new CSVReader();
break;
case 'text_cached': DataReader = new CachedCSVReader();
break;
case 'sql': DataReader = new SQLReader();
break;
case 'sql_cached': DataReader = new CachedSQLReader();
break;
}
}

This breaks the Single Responsibility Principle, which is one of the SOLID principles. Because it is now doing too many things

  1. Presentation logic
  2. Picking the data source (hardcoded web service url)
  3. Managing object lifetime
  4. Deciding when to use a cache

Repository Pattern

Mediates between the domain and data mapping layers using a collection-like interface for accessing domain objects.

It separates our application from the data storage technology.

In other words, we can say that a Repository Design Patternacts as a middleman or middle layer between the rest of the application and the data access logic. That means a repository pattern isolates all the data access code from the rest of the application. The advantage of doing so is that, if you need to do any change then you need to do in one place. Another benefit is that testing your controllers becomes easy because the testing framework need not run against the actual database access code.

The idea is that the repository knows how to communicate with the data store whether it is using HTTP, reading a file from the file system, or making a database call. It then takes the data that comes back and turns it into normal C# objects that the rest of the application can understand. This is exactly what the service reader does now. It makes a HTTP request to the web service, then parses the JSON result into Person objects that the application can use.

CRUD Repository

The Interface Segregation Principle says that interfaces should only contain what the client needs. Normally a Repository should contain all Create, Read, Update and Delete. But in this case we only need Read.

Using Dependency Injection to Build Loosely-coupled Application

Create a new interface

1
2
3
4
5
public interface IPersonReader
{
IEnumerable<Person> GetPeople();
Person GetPerson(int id);
}

In the Presentation layer, inject IPersonReader

1
2
3
4
public PeopleViewModel(IPersonReader dataReader)
{
DataReader = dataReader;
}

Now we don’t create new ServiceReader in the Presentation layer, instead we make it someone else’s responsibility by adding IPersonReader to a contrcutor parameter.

IPersonReader could be ServiceReader or SQLReader or CSVReader but PeopleViewModel doesn’t care.

IPersonReader need to be created before creating PeopleViewModel.

In the UI layer, inject PeopleViewModel

1
2
3
4
5
6
public PeopleViewerWindow(PeopleViewModel peopleViewModel)
{
InitializeComponent();
viewModel = peopleViewModel;
this.DataContext = viewModel;
}

Similar things happened in UI layer, now we don’t create new PeopleViewModel, instead we inject it to the constrctor.

PeopleViewModel need to be created before creating PeopleViewerWindow but it doesn’t care, as long as it is being passed to the constructor.

Dependency Inversion Principle

This is Dependency Inversion Principle in action, now the View Model and the Viewer Window are no longer responsible for creating or managing the lifetime of the dependencies. Instead, the dependency, the data reader, is given to the View Model and the Viewer Window to use.

Object composition

1
2
3
4
5
6
private static void ComposeObjects()
{
var serviceReader = new ServiceReader();
var peopleViewModel = new PeopleViewModel(serviceReader);
Application.Current.MainWindow = new PeopleViewerWindow(peopleViewModel);
}

The ServiceReader and PeopleViewModel objects have been created when the app starts. This code will run OnAppStarts.

Get Data from CSV file

Because now the presentation layer and the data access layer are loosely coupled, if we want to change the data source, we could just create a CSVReader() instead of ServiceReader()

1
2
3
4
5
6
7
private static void ComposeObjects()
{
//var serviceReader = new ServiceReader();
var serviceReader = new CSVReader();
var peopleViewModel = new PeopleViewModel(serviceReader);
Application.Current.MainWindow = new PeopleViewerWindow(peopleViewModel);
}

We just need to implement CSVReader class and create a new CSV object. We don’t need to change any existing code.

Decorator pattern

Wrap an existing interface to add functionality

The idea is we wrap an existing data reader, add the caching functionality and then expose the same data reader interface to the outside world.

Our service reader implements the IPersonReader interface. We take that service reader and wrap it in a caching reader. This adds the caching funcationality that we need. The caching reader is also an IPersonReader. So it looks just like any other data reader to the rest of the application. By using a Decorator, we can wrap any of our existing data readers.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class CachingReader : IPersonReader
{
private IPersonReader _wrappedReader;
private TimeSpan _cacheDuration = new TimeSpan(0, 0, 30);

private IEnumerable<Person> _cachedItems;
private DateTime _dataDateTime;

public CachingReader(IPersonReader wrappedReader)
{
_wrappedReader = wrappedReader;
}

public IEnumerable<Person> GetPeople() {...}

public Person GetPerson(int id) {...}

private bool IsCacheValid {...}

private void ValidateCache() {...}

private void InvalidateCache() {...}
}

In this implementation, CachingReader implements IPersonReader, so it also has GetPeople and GetPerson functions. But it has some other functions (extra caching funcationality).

1
2
3
4
5
6
7
private static void ComposeObjects()
{
var wrappedReader = new ServiceReader();
var reader = new CachingReader(wrappedReader);
var peopleViewModel = new PeopleViewModel(reader);
Application.Current.MainWindow = new PeopleViewerWindow(peopleViewModel);
}

When we want to use the CachingReader, we could first create a ServiceReader, so it has GetPeople and GetPerson function, and we inject this ServiceReader to the CachingReader’s contructor, which adds the extra caching funcationality.

This follows the Open/Closed Principle. Existing data readers can be extended without being modified.

This also follows the Liskov substitution principle. This principle says that descendent classes (CachingReader) should behave the same way the base class (ServiceReader) behave. Meaning we could substitude a child class (CachingReader) for a base class (ServiceReader) in our application, and the application does not know the difference.

We have extended the behavior in the child class, but the calling code does not know the difference.

Unit Testing with Dependency Injection

Before when we need to test the ViewModel, the data service needs to run. Because of the old code looks like this.

1
2
3
4
5
6
7
public PeopleViewModel() {
DataReader = new ServiceReader();
}

public class ServiceReader() {
WebClient client = new WebClient();
}

Now we can just create a fake Reader and provide some fake Person data. The test of ViewModel is isolated from the Data Store.

1
2
3
4
5
6
7
8
9
10
11
12
[Test]
public void Test() {
// Arrange
IPersonReader reader = GetFakeReader();
var viewModel = new PeopleViewModel(reader);

// Act
viewModel.RefreshPeople();

// Assert
...
}

Below is the real unit test code. By passing the FakeReader() to the PeopleViewModel(), we don’t need to create WebClient any more, which makes it easier to write unit test.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[TestMethod]
public void People_OnRefreshPeople_IsPopulated()
{
// Arrange
var reader = new FakeReader();
var viewModel = new PeopleViewModel(reader);

// Act
viewModel.RefreshPeople();

// Assert
Assert.IsNotNull(viewModel.People);
Assert.AreEqual(2, viewModel.People.Count());
}

Next we want to test CSVReader(), but we need to put a real CSV data file in the project directory to make it work because it is expecting a filePath.

1
2
3
4
5
public CSVReader()
{
string filePath = AppDomain.CurrentDomain.BaseDirectory + "People.txt";
FileLoader = new CSVFileLoader(filePath);
}

Luckly, the FileLoader is a public property which can be overrided.

1
public ICSVFileLoader FileLoader { get; set; }

So we could create a FakeFileReader that provides the fake data. We override the property with our own behavior so it doesn’t depend on the file system. This is called Property Injection

1
2
3
4
5
6
7
8
9
10
[TestMethod]
public void GetPeople_WithGoodRecords_ReturnsAllRecords()
{
var reader = new CSVReader();
reader.FileLoader = new FakeFileLoader("Good");

var result = reader.GetPeople();

Assert.AreEqual(2, result.Count());
}

Property Injection

Class property is initialized for standard behavior. By default, the standard behavior is used. Property can be set to provide alternate behavior.

One question is, why not use Constructor Injection like what we did in the previous charpter? Instead of creating CSVFileLoader in the CSVReader constructor, we could inject FileLoader.

This is because we only use FakeFileLoader when doing unit test. In production, it will use CSVFileLoader 100% of the time.

So Constructor injection is only good for when we want to force a decision on a dependency. Property injection is good for when we have a default dependency (CSVFileLoader) that we want to use most of the time.

Dependency Injection Containers