Saturday, September 30, 2017

Using the Built-In Dependency Injection in ASP.NET Core 2.0

ASP.NET Core has a built-in Dependency Injection (DI) container that takes care of things for us with very little code. As an experiment, I added DI to the WebAPI service that I created a couple weeks ago. Let's walk through that process to see what we need to do.

The code is available in GitHub on the "di" branch of the person-api-core project.

Note: .NET Core 2.0 Articles are collected here: Getting Started with .NET Core 2.0.

The Goal
Our goal is to break some tight-coupling in our WebAPI controller. Right now, it relies on a static People class -- this is where the data comes from. Instead, I'd like the controller to rely on an abstraction (an interface in this case). This allows us to easily swap out the data source, and this is great for unit testing. (We'll talk about unit testing in upcoming articles.)

We'll break the tight-coupling by using constructor injection, just by adding a constructor parameter. Once we do that, we configure the ASP.NET Core DI container to resolve that parameter. That's it.

The Current State
The current state of our application isn't the greatest for breaking the coupling. There are a few things we'll need to do to get the code into better shape to support it.

The initial code is available on the "master" branch of the project: GitHub - jeremybytes/person-api-core.

Here's what our WebApi Controller looks like now (from Controllers/PeopleController.cs):

What I'd like to do is get rid of the reliance on the concrete "People" class (which is a static class at this point). What we really need is access to a "GetPeople()" method to retrieve our data. But where this particular method comes from isn't important to the controller itself. And this is one of the cool things about DI that we'll see in a bit.

Here's the current People class (from Models/People.cs):

This is a static class with a static method. And the data that's returned is just hard-coded. We'll have to make a few changes to this class. The main thing is that I want to implement an interface to give us an abstraction to work with. But static classes and static members don't work with interfaces. So some minor surgery will be necessary.

Finally, we have the Startup class (in Startup.cs). Specifically, here's the ConfigureServices method:

This is the default that we get when we create a WebAPI project using the .NET Core 2.0 template. The services collection is where we can add our DI configuration. I won't go into the details of the ASP.NET Core pipeline (because there are lots of people more qualified that I am to talk about that). We'll see the changes we need to make to this method way at the end.

Removing the Static
As mentioned above, I'd like to use an interface to create an abstraction between the controller and the item that provides the data. Unfortunately, our data class "People" is currently a static class with a static method. So we'll have to change that first.

It's a pretty easy change to the "People" class. We just remove "static" from the class and the method:

But now our controller won't work. We'll need to make a few changes to our "PeopleController" class.

Since "People" is no longer static, we can't simply call "People.GetPeople()". I made a change that requires the fewest code updates. This is "new"ing up the "People" class and then immediately calling the "GetPeople" method on that instance.

I really don't like this syntax. I like to keep my "new" (creation) separate from method calls (usage). Since we don't have an intermediate variable that holds the item we created, we have no way to get to this instance of the "People" class again. It's not really a problem in this case, but I'd hate to get into the habit of it.

This is just a personal preference. There are samples on the Microsoft Docs site that use this syntax.

The "People" class is also used in the "Get(int id)" method. So I made a similar change here:

Once we get the interface in place, we'll make a few more changes to this class (and get rid of the syntax that's bothering me).

But the important thing right now is that we can build and run our WebAPI service. (For instructions on how to run/call the service, see the prior article about building a WebAPI service.) We don't have any looser coupling, but we did get rid of the static class and method that prevented us from moving forward.

Adding the Interface
Now that we have an instance class and method, we can create an interface for that. Here's the interface file that we created, Models/IPeopleProvider.cs (note that we've flipped over to the "di" branch in the GitHub project for this code):

I called this "IPeopleProvider". I'm not very happy with the naming. But as we all know, naming really sucks.

This interface only has one member "GetPeople()". This gives us the abstraction that we need to move forward.

Now we just have to mark that the "People" class implements this interface:

We don't have to make any other changes since the People class already has the "GetPeople" method. But now, I want to change the name of the class.

Renaming a Bit
I want to change the name of the "People" class because it isn't descriptive enough now. With the changes that we're making, it's implied that we will have different implementations of the "IPeopleProvider" interface. So I want this particular implementation to be a bit more descriptive.

For this, we'll change the name of the class to "StaticPeopleProvider". So we'll rename the file and the class itself.

Again, I'm not completely happy with this name. Since the data coming from this class is hard-coded (as opposed to coming from a database, file, or service), I used "static" as part of the name. Hopefully, this isn't too confusing since we just removed the "static" attribute of the class. "HardCodedPeopleProvider" might be better, but that didn't sound right to me either.

Anyway, there's always room for improvement, particularly with naming.

Since we renamed that class, we'll also need to update the controller:

This ensures that our code still builds and runs. I really like to keep non-building code to a minimum. And if things still run, I have a lot more confidence that I haven't strayed too far.

This is another reason I like to have unit tests in place -- to make sure I haven't strayed too far. Unfortunately for this service and the changes that we're making to it, unit tests wouldn't help too much with that. We always have to find a good tool for the job.

Constructor Injection
Now that we have the pieces in place, we can write the code that will give us the loose coupling. For this, we'll use a pattern known as constructor injection.

We have a dependency on the "StaticPeopleProvider" class. In particular, we need to call the "GetPeople" method on that class in order for our code to work. Right now we're handling that dependency ourselves (by "new"ing up an instance of the class).

Rather than being responsible for that dependency, we'd like someone else to provide it for us. In this case, we'll ask someone else to provide the dependency as a constructor parameter.

Here are a couple updates to our "PeopleController" (in Models/PeopleController.cs):

We've done two things here. First we've added a private field to hold our "IPeopleProvider". We don't care about what the concrete type of this object is, all we care about is that it has a "GetPeople" method that we can call in our code. And that's exactly what the interface gives us.

Next, we've added a constructor to our class that takes an "IPeopleProvider" as a parameter. This means that whoever creates our controller class must also provide us with an instance of a class that implements that interface. We then assign that parameter to our private field.

Using the Dependency
Now that we have the "provider" field in our class, we can update our methods to use it. (This will also get rid of that syntax I don't like.)

Here are the updated "Get" methods (also in Models/PeopleController.cs):

Instead of "new"ing up items, we use the "GetPeople" method on our private field. The result is that our controller class doesn't need to know anything about the "StaticPeopleProvider" class or any other concrete class. It only needs to know about the abstraction (the interface).

Injecting Dependencies
We have't removed our dependencies: we still need an object with a "GetPeople" method on it. But instead of handling that dependency ourselves (in the controller), we're now injecting the dependency (hey, that's "Dependency Injection") through the constructor. And since we're injecting it through the constructor, we call this pattern "Constructor Injection".

Right now, our service will build, but it will not run. Our last step will be to configure the DI container.

Configuring the Container
Our controller is now all set up for dependency injection, but we need to get that dependency in there somehow. Since we're using the DI container that's built-in to ASP.NET Core, we just need to make a change to our Startup class (from Startup.cs):

In the "ConfigureServices" method, we just need to add one line of code. This code associates an abstraction (IPeopleProvider) with a concrete type (StaticPeopleProvider).

The method "AddSingleton" means that we end up with one and only one instance of the "StaticPeopleProvider" class. That works in our case particularly since we have hard-coded data.

Other options include "AddTransient", which means that we get a new instance each time we ask for one, and "AddScoped", which means that we get a new instance for each HTTP Request. I won't go into those details because other folks have done a pretty good job with that.

Now that the container is configured, our code will build and run just fine. To run the service, we can just type "dotnet run" at a command prompt:

Then we can navigate to the service in the browser (in this case: http://localhost:9874/api/people):

It looks kind of magical, so let's take a step back to see how this works.

How It Works
It feels like some code is missing here, but the ASP.NET Core DI container takes care of the details for us. This is a big reason to use a container (and also why they can be confusing); they are a bit magical.

When we navigate to the service with the URL, the router figures out that we need an instance of the "PeopleController" class. When we look at the constructor, we find that we need a parameter:

The DI container says, "I need an IPeopleProvider. Do I know what that is?" It looks through the configuration and finds a mapping:

The DI container then says, "Okay, so if someone asks for an IPeopleProvider, I'm supposed to give them a StaticPeopleProvider."

Then it finds the StaticPeopleProvider:

This doesn't have an explicit constructor, so the DI container creates an instance of the StaticPeopleProvider using the default constructor. Then it passes that object to the constructor of the "PeopleController" class.

Once we have an instance of the "PeopleController" class, the appropriate "Get" method is called:

The "Get" method then returns that data based on the "StaticPeopleProvider" class that was injected through the constructor.

More Information
There are several good articles on ASP.NET Core dependency injection. The one on Microsoft docs has quite a bit information (though it might be a bit difficult if you're new to DI): Introduction to Dependency Injection in ASP.NET Core. This also shows how to swap out the built-in container for a third-party one (such as Autofac).

And Shawn Wildermuth has some great information (as usual): ASP.NET Core Dependency Injection.

And if you want to get a better grasp on the concepts behind Dependency Injection, I've got lots of articles on my website: DI Why? Getting a Grip on Dependency Injection. And I've also got a Pluralsight course on the subject: Dependency Injection On-Ramp.

Wrap Up
I'm glad to see that getting started with the built-in DI container in ASP.NET Core goes pretty quickly. I'm sure that there are quite a few nuances, and I'll be digging in a bit deeper. Constructor Injection works quite well. Property Injection is not currently supported, but other things such as Parameter Injection are. There's quite a bit more exploring to do.

Happy Coding!

No comments:

Post a Comment