Inversion of control and its role
Software needs structure really fast when growing beyond one page of source code. Typically, you’d group things logically in types that have a specific purpose in your system. With your software being broken up for better maintainability, the different parts are then often dependent on each other to be able to perform the overall tasks you need it to do.
Building a module for registering users
Let’s build a simple system that handles a user sign-up feature exposed as a REST API. Start by creating a folder called Chapter10. Change into this folder in your command line and create a new web-based project:
dotnet new web
The type of information you’d want to capture involves both personal information and also the user’s credentials that we want to have as part of the body of our API. Add a file called RegisterUser.cs and add the following to it:
namespace Chapter10; public record RegisterUser(string FirstName, string LastName, string SocialSecurityNumber, string UserName, string Password);
The RegisterUser type takes all the different properties you want to capture for the user for the API. This is not what you want to store directly in a database. When you store this, you want to store this as two separate things – the user credentials and the user details. Create a file called User.cs and add the following to it:
namespace Chapter10; public record User(Guid Id, string UserName, string Password);
The User type only captures the actual user name and the password and has a unique identifier for the user. Then add a file called UserDetails and add the following to it:
namespace Chapter10; public record UserDetails(Guid Id, Guid UserId, string FirstName, string LastName, string SocialSecurityNumber);
UserDetails holds the rest of the information we will be getting from the RegisterUser type.
The next thing we need is an API controller to take this and store the information in the database. We will be using MongoDB as a backing store.
We will be relying on a third-party library to access MongoDB. Add the package to the project by running the following in the terminal:
dotnet add package mongodb.driver
Create a file called UsersController and add the following to it:
using Microsoft.AspNetCore.Mvc; using MongoDB.Driver; namespace Chapter10; [Route("/api/users")] public class UsersController : Controller { IMongoCollection<User> _userCollection; IMongoCollection<UserDetails> _userDetailsCollection; public UsersController() { var client = new MongoClient ("mongodb://localhost:27017"); var database = client.GetDatabase("TheSystem"); _userCollection = database.GetCollection<User> ("Users"); _userDetailsCollection = database.GetCollection <UserDetails>("UserDetails"); } [HttpPost("register")] public async Task Register([FromBody] RegisterUser userRegistration) { var user = new User(Guid.NewGuid(), userRegistration.UserName, userRegistration.Password); var userDetails = new UserDetails(Guid.NewGuid(), user.Id, userRegistration.FirstName, userRegistration.LastName, userRegistration .SocialSecurityNumber); await _userCollection.InsertOneAsync(user); await _userDetailsCollection.InsertOneAsync (userDetails); } }
The code sets up in its constructor the database and gets the two different collections in which we will be storing the user information coming in. The register API method then takes RegisterUser and splits it up into the two respective types and inserts them into each of their MongoDB collections.
Important note
In a real system, you would obviously encrypt the password with a strong (preferably one-way) encryption strategy and not just store the password as clear text.
Open your Program.cs file and make it look like the following:
var builder = WebApplication.CreateBuilder(args); builder.Services.AddControllers(); var app = builder.Build(); app.UseRouting(); app.UseEndpoints(_ => _.MapControllers()); app.Run();
Before you run the solution so far, you need to start the MongoDB server. You do this by using Docker. In your terminal, run the following:
docker run -d -p 27017:27017 mongo
The command should start MongoDB as a background daemon and expose port 27017 so that you can connect to it. You should see something similar to the following line:
9fb4b3c16d7647bfbb69eabd7863a169f6f2e4218191cc69c7454978627 f75d5
This is the unique identifier of the running Docker image.
You can now run the code you’ve created so far from your terminal:
dotnet run
You should now see something similar to the following:
info: Microsoft.Hosting.Lifetime[14] Now listening on: http://localhost:5000 info: Microsoft.Hosting.Lifetime[0] Application started. Press Ctrl+C to shut down. info: Microsoft.Hosting.Lifetime[0] Hosting environment: Development info: Microsoft.Hosting.Lifetime[0] Content root path: /Users/einari/Projects/ Metaprogramming-in-C/Chapter10/
Testing the API
With the code thus far, you now have an API that has a route of /api/users/register that accepts an HTTP POST.
You can test your API by using Postman with the following steps:
- Select POST.
- Enter the URL for the API – http://localhost:5000/api/user/register.
- In the Body tab, select Raw as the input and then JSON as the type.
Important note
The port of the URL has to match the port in the output where it says Now listening on: http://localhost:{your port}.
Figure 10.1 – Testing the API using Postman
Once you’ve clicked Send, you should get 200 OK at the bottom. Then you can open the MongoDB editor – for instance, Compass, as suggested in the pre-requisites.
Create a new connection to the MongoDB server and perform the following steps:
- Make sure the connection string is pointing to your MongoDB server. By default, it should say mongodb://localhost:27017, which matches the code.
- Click the Connect button.
Figure 10.2 – Creating a new connection
Once connected, you should see the database TheSystem on the left-hand side and, within it, the collections. Clicking the user collection or user-details, you should see the data you registered on the right side.
Figure 10.3 – Registered data
This is all fine, and the code certainly does its job as expected. But the code could be improved.
Refactoring the code
There are a couple of challenges with this type of code:
- Firstly, the controller is taking on the responsibility for the infrastructure
- Secondly, it also takes on the responsibility for the actual domain logic and knowing exactly how to store things in a database
An API surface should instead just rely on other subsystems to do their specific job and then delegate to them rather and then become a composition.
For instance, we could go and isolate the user credential registration and the user details registration into two different services that we could use.
Creating services
Let’s pull it apart a little bit and start putting in some structure. Create a file called UsersService.cs and make it look like the following:
using MongoDB.Driver; namespace Chapter10; public class UsersService { readonly IMongoCollection<User> _usersCollection; public UserService() { var client = new MongoClient ("mongodb://localhost:27017"); var database = client.GetDatabase("TheSystem"); _usersCollection = database.GetCollection<User> ("Users"); } public async Task<Guid> Register(string userName, string password) { var user = new User(Guid.NewGuid(), userRegistration.UserName, userRegistration .Password); await _usersCollection.InsertOneAsync(user); return user.Id; } }
The code is doing exactly the same as it did in UsersController for registering the user, just that it is now formalized as a service. Let’s do the same for the user details. Create a file called UserDetailsService.cs and make it look like the following:
namespace Chapter10; public class UserDetailsService { readonly IMongoCollection<User> _userDetailsCollection; public UserDetailsService(IDatabase database) { var client = new MongoClient ("mongodb://localhost:27017"); var database = client.GetDatabase("TheSystem"); _userDetailsCollection = database.GetCollection <User>("UserDetails"); } public Task Register(string firstName, string lastName, string socialSecurityNumber, Guid userId) => _userDetailsCollection_.InsertOneAsync (new(Guid.NewGuid(), userId, firstName, lastName, socialSecurityNumber)); }
As with UsersService, the code does exactly the same as the original code in UsersController, only now separated out and focused.
This is a great step. Now the infrastructure details of the database are hidden from the outside world and anyone wanting to register a user only has to focus on the information needed to do so and not how it’s done.
The next step is for you to change UsersController to leverage the new services.
Changing the controller
Go and change the controller to look like the following:
[Route("/api/users")] public class UsersController : Controller { readonly UsersService _usersService; readonly UserDetailsService _usersDetailsService; public UsersController() { _usersService = new UsersService(); _userDetailsService = new UserDetailsService(); } [HttpPost("register")] public async Task Register([FromBody] RegisterUser userRegistration) { await _usersService.Register( userRegistration.UserName, userRegistration.Password); await _userDetailsService.Register( userRegistration.FirstName, userRegistration.LastName, userRegistration.SocialSecurityNumber); } }
The code creates an instance of the UsersService class in the constructor and uses the Register method directly in the Register API method.
If you run the sample at this point and perform the HTTP POST again, you will get the exact same result.
UsersService and UserDetailsService are now dependencies that UsersController have and it creates those dependencies as instances itself. There are a couple of downsides to this. The dependencies are basically now following the life cycle of the controller. Since controllers are created once per web request, it means UsersService and UserDetailsService will be created every time as well. This could be a performance issue, and is not really a problem the controller should be worried about. Its main job is just to provide an API surface for registering users.
It’s also very hard to be able to write tests for UsersController, as the dependencies are now hard-wired and it brings in all the infrastructure with it and then makes it much harder to test the logic of UsersController in isolation.
This is where dependency inversion comes in, by reversing the relationship and saying that the system, in our case UsersController, is not responsible for creating the instance itself, but rather has it as an argument to the constructor, and letting whoever is instantiating the controller be responsible for providing the dependencies UsersController has.
Change UsersController to take the dependency on the constructor:
[Route("/api/users")] public class UsersController : Controller { readonly UsersService _usersService; readonly UserDetailsService _usersDetailsService; public UsersController( UsersService usersService, UserDetailsService userDetailsService) { _usersService = usersService; _userDetailsService = userDetailsService; } [HttpPost("register")] public async Task Register([FromBody] RegisterUser userRegistration) { await _usersService.Register( userRegistration.UserName, userRegistration.Password); await _userDetailsService.Register( userRegistration.FirstName, userRegistration.LastName, userRegistration.SocialSecurityNumber); } }
The code now takes UsersService and UserDetailsService as arguments and uses those directly instead of creating an instance of them itself.
We now have the benefit of the dependencies being very clear to the outside world. The life cycle of UsersService can then be managed outside of the controller.
However, since the controller is taking the concrete instances, it is still tied to the infrastructure. This can be improved upon to decouple the infrastructure and make it more testable.
Contract oriented
To improve further on this, we could also extract the content of UsersService and UserDetailsService into interfaces and use those instead. The benefits of that are that you would decouple from the concrete implementation and its infrastructure needs and add flexibility in your code by allowing different implementations and, depending on the configuration or the system being in a specific state, switch out which implementation of the interface to use.
An additional benefit of extracting into an interface is that you make it easier to write tests that focus purely on the unit being tested and only the interaction with its dependencies, without having to bring in the entire infrastructure to write the automated test.
Create a file called IUsersService.cs and make it look like the following:
namespace Chapter10; public interface IUsersService { Task<Guid> Register(string userName, string password); }
The code holds the Register method with the same signature as in the original UsersService class. Then the implementation of UsersService only changes by adding the IUsersService inheritance. Open the UsersService file and make it implement the IUsersService interface:
public class UsersService : IUsersService { /* Same code as before within the UsersService */ }
For UserDetailsService, we want to do the same. Add a file called IUserDetailsService.cs and make it look like the following:
namespace Chapter10.Structured; public interface IUserDetailsService { Task Register(string firstName, string lastName, string socialSecurityNumber, Guid userId); }
The code holds the Register method with the same signature as in the original UserDetailsService class. Then the implementation of UserDetailsService only changes by adding the IUserDetailsService inheritance. Open the UserDetailsService file and make it implement the IUserDetailsService interface:
public class UserDetailsService : IUserDetailsService { /* Same code as before within the UserDetailsService */ }
With these two changes, we can now change how we express the dependencies. In UsersController, you then change from using UsersService to IUsersService and UserDetailsService to IUserDetailsService:
[Route("/api/users")] public class UsersController : Controller { readonly IUsersService _usersService; readonly IUserDetailsService _userDetailsService; public UsersController( IUsersService usersService, IUserDetailsService userDetailsService) { _usersService = usersService; _userDetailsService = userDetailsService; } // Same register API method as before would go here }
The code now takes the two IUsersService and IUserDetailsService dependencies using their interfaces and the rest of the code remains unchanged.
So far, we’ve discussed dependencies and the benefits of the dependency inversion principle. Still, we need to be able to provide these dependencies. And it is very impractical if we have to manually provide these all around our system and maintain life cycles of them in different ways. It could lead to a very messy, unmaintainable code base and could also lead to unknown side effects.
What you really want is something that manages this for you. This is what is known as an inversion of control container (IoC container). Its job is to hold information about all your services, which implementation is used for what interface, and also the life cycle of these. The IoC container is a centralized piece that you configure at the beginning of your application and after its configuration is done, you can ask it to provide instances of anything that is registered with it. It’s very useful for registering any kind of dependencies, not just the ones where it is an interface to an implementation. You can register concrete types, delegate types, or pretty much anything.
The IoC container works recursively and will deal with dependencies of dependencies and resolve everything correctly.
In ASP.NET Core, the concept of an IoC container is already set up out of the box and is really easy to use with what is known as ServiceCollection, where you can set up all the service registrations.