How YOU can Learn Dependency Injection in .NET Core and C#

Chris Noring - Nov 28 '19 - - Dev Community

Follow me on Twitter, happy to take your suggestions on topics or improvements /Chris

This is an intro to Dependency Injection, also called DI. I plan a follow-up post covering more advanced scenarios. For now, I want to explain what it is and how you can use it and finally show how it helps to test A LOT.

What is Dependency Injection

It's a programming technique that makes a class independent of its dependencies.

English?

We don't rely on a concrete implementation of our dependencies, but rather interfaces. This makes our code more flexible and we can easily switch out a concrete implementation for another while maintaining the same logic.

References

 Why use it

There are many advantages:

  • Flexible code, we can switch out one implementation for another without changing the business logic.
  • Easy to test, because we rely on interfaces over implementations - we can more easily test our code without worrying about side-effects. We will show this later in the article.

 DI in .NET Core - it's built-in

There's a built-in Dependency Injection container that's used by a lot of internal services like:

  • Hosting Environment
  • Configuration
  • Routing
  • MVC
  • ApplicationLifetime
  • Logging

The container is sometimes referred to as IoC, Inversion of Control Container.

The overall idea is to Register at the application startup and then Resolve at runtime when needed.

Container responsibilities:

  • Creating
  • Disposing
  • IServiceCollection, Register services, lets the IoC container know of concrete implementation. It should be used to resolve what Interface belongs to what implementation. How something is created can be as simple as just instantiating an object but sometimes we need more data than that.
  • IServiceProvider, Resolve service instances, actually looking up what interface belongs to what concrete implementation and carry out the creation.

It lives in the Microsoft.Extensions.DependencyInjection.

What to register

There are some telltale signs.

  • Lifespan outside of this method?, Are we new-ing the service, any services that can they live within the scope of the method? I.e are they a dependency or not?
  • More than one version, Can there be more than one version of this service?
  • Testability, ideally you only want to test a specific method. If you got code that does a lot of other things in your method, you probably want to move that to a dedicated service. This moved code would then become dependencies to the method in question
  • Side-effect, This is similar to the point above but it stresses the importance of having a method that does only one thing. If a side-effect is produced, i.e accessing a network resource, doing an HTTP call or interacting with I/O - then it should be placed in a separate service and be injected in as a dependency.

Essentially, you will end up moving out code to dedicated services and then inject these services as dependencies via a constructor. You might start out with code looking like so:

public void Action(double amount, string cardNumber, string address, string city, string name) 
{
  var paymentService = new PaymentService();
  var successfullyCharged = paymentService.Charge(int amount, cardNumber);

  if (successfullyCharged) 
  {
    var shippingService = new ShippingService();
    shippingService.Ship(address, city, name);
  }
}
Enter fullscreen mode Exit fullscreen mode

The above has many problems:

  • Unwanted side-effects when testing, The first problem is that we control the lifetime of PaymentService and ShippingService, thus risking firing off a side-effect, an HTTP call, when trying to test.
  • Can't test all paths, we can't really test all paths, we can't ask the PaymentService to respond differently so we can test all execution paths
  • Hard to extend, will this PaymentService cover all the possible means of payment or would we need to add a lot of conditional code in this method to cover different ways of taking payment if we added say support for PayPal or a new type of card, etc?
  • Unvalidated Primitives, there are primitives like double and string. Can we trust those values, is the address a valid address for example?

From the above, we realize that we need to refactor our code into something more maintainable and more secure. Turning a lot of the code into dependencies and replacing primitives with more complex constructs - is a good way to go.

The result could look something like this:

class Controller 
  private readonly IPaymentService _paymentService;
  private readonly IShippingService _shippingService;

  public void Controller(
    IPaymentService paymentService,
    IShippingService shippingService
    ) 
  {
    _paymentService = paymentService;
    _shippingService = shippingService;
  }

  public void Action(IPaymentInfo paymentInfo, IShippingAddress shippingAddress) 
  {
    var successfullyCharged = _paymentService.Charge(paymentInfo);

    if (successfullyCharged) 
    {
      _shippingService.Ship(ShippingAddress);
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Above we have turned both the PaymentService and ShippingService into dependencies that we inject in the constructor. We also see that all the primitives have been collected into the complex structures IShippingAddress and IPaymentInfo. What remains is pure business logic.

Dependency Graph

When you have a dependency it might itself rely on another dependency being resolved first and so on and so forth. This means we get a hierarchy of dependencies that need to be resolved in the right order for things to work out. We call this a Dependency Graph.

DEMO - registering a Service

We will do the following:

  • create a .NET Core solution
  • add a webapi project to our solution
  • fail, see what happens if we forgot to register a service. It's important to recognize the error message so we know where we went wrong and can fix it
  • registering a service, we will register our service and we will now see how everything works

Create a solution

mkdir di-demo
cd di-demo

dotnet new sln
Enter fullscreen mode Exit fullscreen mode

this will create the following structure:

-| di-demo
--------| di-demo.sln
Enter fullscreen mode Exit fullscreen mode

Create a WebApi project

dotnet new webapi -o api
dotnet sln add api/api.csproj
Enter fullscreen mode Exit fullscreen mode

The above will create a webapi project and add it to our solution file.

Now we have the following structure:

-| di-demo
--------| di-demo.sln
--------| api/
Enter fullscreen mode Exit fullscreen mode

fail

First, we will compile and run our project so we type:

dotnet run
Enter fullscreen mode Exit fullscreen mode

The first time you run the project the web browser might tell you something like your connection is not secure. You have a dev cert that's not trusted. Fortunately, there's a built-in tool that can fix this so you can run a command like this:

dotnet dev-certs https --trust
Enter fullscreen mode Exit fullscreen mode

For more context on the problem:

https://www.hanselman.com/blog/DevelopingLocallyWithASPNETCoreUnderHTTPSSSLAndSelfSignedCerts.aspx

You should have something like this running:

Ok then, we don't have an error but let's introduce one.

Let's do the following:

  1. Create a controller that supports getting products, this should inject a ProductsService
  2. Create a ProductsService, this should be able to retrieve Products from a data source
  3. **Create a IProductsService interface, inject this interface in the controller

Add a ProductsController

Add the file ProductsController.cs with the following content:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Services;

namespace api.Controllers
{
  [ApiController]
  [Route("[controller]")]
  public class ProductsController : ControllerBase
  {
    private readonly IProductsService _productsService;
    public ProductsController(IProductsService productsService) {
      _productsService = productsService;
    }

    [HttpGet]
    public IEnumerable<Product> GetProducts() 
    {
      return _productsService.GetProducts();
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Note how we inject the IProductsService in the constructor. This file should be added to the Controllers directory.

Add a ProductsService

Let's create a file ProductsService.cs under a directory Services, with the following content:

using System;
using System.Collections.Generic;
using System.Linq;

namespace Services {
  public class Product {
    public string Title { get; set; }
  }

  public class ProductsService: IProductsService 
  {
    private readonly List<Product> Products = new List<Product> 
    { 
      new Product { Title= "DVD player" },
      new Product { Title= "TV" },
      new Product { Title= "Projector" }
    };

    public IEnumerable<Product> GetProducts() 
    {
      return Products.AsEnumerable();
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Create an interface IProductsService

Let's create the file IProductsService.cs under the Services directory, with the following content:

using System;
using System.Collections.Generic;

namespace Services 
{
  public interface IProductsService
  {
      IEnumerable<Product> GetProducts();
  }
}
Enter fullscreen mode Exit fullscreen mode

Run

Let's run the project with:

dotnet build
dotnet run
Enter fullscreen mode Exit fullscreen mode

We should get the following response in the browser:

It's failing, just like we planned. Now what?

Well, we fix it, by registering it with our container.

Registering a service

Ok, let's fix our problem. We do so by opening up the file Startup.cs in the project root. Let's find the ConfigureServices() method. It should have the following implementation currently:

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();
}
Enter fullscreen mode Exit fullscreen mode

Let's change the code to the following:

public void ConfigureServices(IServiceCollection services)
{
    services.AddTransient<IProductsService, ProductsService>();
    services.AddControllers();
}
Enter fullscreen mode Exit fullscreen mode

The call to services.AddTransient() registers IProductsService and associates it with the implementing class ProductsService. If we run our code again:

dotnet run
Enter fullscreen mode Exit fullscreen mode

Now your browser should be happy and look like this:

Do we know all we need to know now?

No, there's lots more to know. So please read on in the next section to find out about different lifetimes, transient is but one type of lifetime type.

Service lifetimes

The service life time means how long the service will live, before it's being garbage collected. There are currently three different lifetimes:

  • Transient, services.AddTransient(), the service is created each time it is requested
  • Singleton, services.AddSingleton(), created once for the lifetime of the application
  • Scoped, services.AddScoped(), created once per request

So when to use each kind?

Good question.

Transient

So for Transient, it makes sense to use when you have a mutable state and it's important that the service consumer gets their own copy of this service. Also when thread-safety is not a requirement. This is a good default choice when you don't know what life time to go with.

 Singleton

Singleton means that we have one instance for the lifetime of the application. This is good if we want to share state or creating the Service is considered expensive and we want to create it only once. So this can boost performance as it's only created once and garbage collected once. Because it can be accessed by many consumers thread-safety is a thing that needs to be considered. A good use case here is a memory cache but ensure you are making it thread-safe. Read more here about how to make something thread-safe:

http://www.albahari.com/threading/part2.aspx

Read especially about the lock keyword in the above link.

Scoped

Scoped means it's created once per request. So all calling consumers within that request will get the same instance. Examples of scoped services are for example DbContext for Entity Framework. It's the class we use to access a Database. It makes sense to make it scoped. We are likely to do more than one call to it during our request and the resource should be scoped to that specific request/user.

 Here be dragons

There's such a thing as captured dependencies. This means that a service lives longer than expected.

So why is that bad?

Well, you want services to live according to their lifetime, otherwise, we take up unnecessary space in memory.

How does it happen?

When you start depending on a Service with a shorter lifetime than yourself you are effectively capturing it, forcing it to stay around according to your lifetime. Example:

You register a ProductsService with a scoped lifetime and an ILogService with a transient lifetime. Then you inject the ILogService into the ProductsService constructor and thereby capturing it.

class ProductsService 
{
  ProductsService(ILogService logService) 
  {

  }
}
Enter fullscreen mode Exit fullscreen mode

Don't do that!

If you are going to depend on something ensure that what you inject has an equal or longer life time than yourself. So either change what you depend on or change the lifetime of your dependency.

Summary

We have explained what Dependency Injection is and why it's a good idea to use it. Additionally, we have shown how the built-in container helps us register our dependencies. Lastly, we've discussed how there are different lifetimes for a dependency and which one we should be choosing.

This was the first part of the built-in container. I hope you are excited about a follow-up post talking about some of its more advanced features.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Terabox Video Player