© Peter Himschoot 2019
Peter HimschootBlazor Revealedhttps://doi.org/10.1007/978-1-4842-4343-5_4

4. Services and Dependency Injection

Peter Himschoot1 
(1)
Melle, Belgium
 

Dependency inversion is one of the basic principles of good object-oriented design. The big enabler is dependency injection. In this chapter, you will look into dependency inversion and injection and why they are fundamental parts of Blazer. You will explore them by building a service that encapsulates where the data gets retrieved and stored.

What Is Dependency Inversion?

Currently your Blazor PizzaPlace app retrieves its data from hard-coded sample data. But in a real-life situation this data will be stored in a database on the server. Retrieving and storing this data can be done in the component itself, but this is a bad idea. Why? Because technology changes quite often, and different customers of your application might want to use their own specific technology, requiring you to update your app for every customer.

Instead you will put this logic into a service object. A service object’s role is to encapsulate specific business rules, especially how data is communicated between the client and server. A service object is also a lot easier to test since you can write unit tests that run on their own, without requiring a user to interact with the application for testing.

But first, let’s talk about the dependency inversion principle and how dependency injection allows us to apply this principle.

Understanding Dependency Inversion

Imagine a component that uses a service. The component creates the service using the new operator, as in Listing 4-1.
@using MyFirstBlazor.Client.Services
<div>
  @foreach(var product in productsService.GetAllProducts())
  {
    <div>@product.Name</div>
    <div>@product.Description</div>
    <div>@product.UnitPrice</div>
  }
</div>
@functions {
  ProductsService productsService = new ProductsService();
}
Listing 4-1

A Component Using a ProductsService

This component is now completely dependent on the ProductsService! You cannot replace the ProductsService without walking over every line of code in your application where the ProductsService is used and replacing it with another class. This is also known as tight coupling (see Figure 4-1).
../images/469993_1_En_4_Chapter/469993_1_En_4_Fig1_HTML.png
Figure 4-1

Traditional layered approach with tight coupling

Now you want to test the ProductList component. ProductsService requires a server on the network to talk to. In this case, you must set up a server just to run the test. And if the server is not ready yet (the developer in charge of the server hasn’t come around to it), you cannot test your component! Or say you are using ProductsService in several places in your location, and you need to replace ProductsService with another class. Now you need to find every use of the ProductsService and replace the class. Maintenance nightmare!

Using the Dependency Inversion Principle

The Dependency Inversion Principle states
  1. A.

    High-level modules should not depend on low-level modules. Both should depend on abstractions.

     
  2. B.

    Abstractions should not depend on details. Details should depend on abstractions.

     

What this means is that the ProductsList component (the higher-level module) should not directly depend on ProductsService (the lower-level module). Instead, it should rely on an abstraction. It should rely on an interface describing what a ProductsService should be able to do, not a class describing how it should work.

The IProductsService interface looks like Listing 4-2.
public interface IProductsService
{
  List<Product> GetAllProducts();
}
Listing 4-2

The Abstraction as Described in an Interface

So change the ProductsList component to rely on this abstraction shown in Listing 4-3.
@using MyFirstBlazor.Client.Services
<div>
  @foreach(var product in productsService.GetAllProducts())
  {
      <div>@product.Name</div>
      <div>@product.Description</div>
      <div>@product.UnitPrice</div>
  }
</div>
@functions {
  IProductsService productsService;
}
Listing 4-3

The ProductList Component Using the IProductsService Interface

Now the ProductList component only relies on the IProductsService interface, an abstraction. Of course, you now make the ProductsService implement the interface as in Listing 4-4.
public class ProductsService : IProductsService
{
  public List<Product> GetAllProducts()
  {
      // some implementation
  }
}
Listing 4-4

The ProductsService Implementing the IProductsService Interface

If you want to test the ProductList component with dependency inversion in place, you can simply build a hard-coded version of ProductsService and run the test without needing a server, as in Listing 4-5. And if you use ProductsService in different places in your application, all you need to do to replace its implementation is to build another class that implements the IProductsService interface and tell dependency injection to use the other class! This is also known as the Open/Closed Principle from SOLID.
public class HardCodedProductsService : IProductsService
{
  public static List<Product> products = new List<Product>
  {
    new Product {
      Id =1,
      Name = "Isabelle's Homemade Marmelade",
      Description = "...",
      UnitPrice = 1.99M }
  };
  public List<Product> GetAllProducts()
  {
    return products;
  }
}
Listing 4-5

A Hard-Coded ProductsService Used for Testing

By applying the Principle of Dependency Inversion (see Figure 4-2), you gained a lot more flexibility.
../images/469993_1_En_4_Chapter/469993_1_En_4_Fig2_HTML.png
Figure 4-2

Loosely coupled objects through dependency inversion

Adding Dependency Injection

If you run your application, now you will get a NullReferenceException. Why? Because the ProductsList component still needs an instance of a class implementing IProductsService! You could pass the ProductsService in the constructor of the ProductList component, for example in Listing 4-6.
new ProductList(new ProductService())
Listing 4-6

Passing the ProductService in the Constructor

But if the ProductsService also depends on another class, it quickly becomes like Listing 4-7.
new ProductList( new ProductService(new Dependency()))
Listing 4-7

Creating a Deep Chain of Dependencies Manually

This is, of course, not a practical way of working! Because of that, you will use an Inversion-of-Control Container (I didn’t invent this name!).

Applying an Inversion-of-Control Container

An Inversion-of-Control Container (IoCC) is just another object that specializes in creating objects for you. You simply ask it to create an instance of a class and it will take care of creating any dependencies required.

It is a little bit like in a movie when a surgeon, in the middle of an operation, needs a scalpel. The surgeon holds out his or her hand and asks for “scalpel number 5.” The nurse (the Inversion-of-Control Container) who is assisting simply hands the surgeon the scalpel. The surgeon doesn’t care where the scalpel comes from or how it was built.

So, how can the IoCC know which dependencies your component needs? There are two ways.

Constructor Dependency Injection

Classes that need a dependency can simply state their dependencies in their constructor. The IoCC will examine the constructor and instantiate the dependencies before calling the constructor. And if these dependencies have their own dependencies, then the IoCC will also build them! For example, if the ProductsService has a constructor that takes an argument of type Dependency, as in Listing 4-8, then the IoCC will create an instance of type Dependency and will then call the ProductsService’s constructor with that instance. The ProductsService constructor then stores a reference to the dependency in some field, as in Listing 4-8. Should the ProductsService's constructor take multiple arguments, then the IoCC will create an instance for each argument. Constructor injection is normally used for required dependencies.
public class ProductsService {
  private Dependency dep;
  public ProductsService(Dependency dep) {
    this.dep = dep;
  }
}
Listing 4-8

The ProductsService’s Contructor with Arguments

Property Dependency Injection

If the class that the IoCC needs to build has properties that indicate a dependency, then these properties are filled in by the IoCC. The way a property does this depends on the IoCC (in .NET there are a couple of different IoCC frameworks), but in Blazor you can have the IoCC inject an instance with the @inject directive in your Razor file, as in the third line of code in Listing 4-9.
@using MyFirstBlazor.Client.Services
@inject IProductsService productsService
<div>
    @foreach(var product in productsService.GetAllProducts())
    {
        <div>@product.Name</div>
        <div>@product.Description</div>
        <div>@product.UnitPrice</div>
    }
</div>
@functions {
}
Listing 4-9

Injecting a Dependency with the @inject Directive

If you’re using code separation, you can add a property to your class and apply the [Inject] attribute as in Listing 4-10.
using System;
using Microsoft.AspNetCore.Blazor.Components;
using MyFirstBlazor.Client.Services;
namespace MyFirstBlazor.Client.Pages
{
  public class ProductListViewModel : BlazorComponent
  {
    [Inject]
    public IProductsService ProductsService { get; set; }
  }
}
Listing 4-10

Using the Inject Attribute for Property Injection

You can then use this property directly in your Razor file, as in Listing 4-11.
@inherits ProductListViewModel
<div>
@foreach (var product in ProductsService.GetAllProducts())
{
  <div>@product.Name</div>
  <div>@product.Description</div>
  <div>@product.UnitPrice</div>
}
</div>
Listing 4-11

Using the ProductsService Property That Was Dependency Injected

Configuring Dependency Injection

There is one more thing I need to discuss. When your dependency is a class, the IoCC can easily know that it needs to create an instance of the class with the class’ constructor. But if your dependency is an interface, which it generally needs to be if you are applying the Principle of Dependency Inversion, then which class does it use to create the instance? Without your help it cannot know.

An IoCC has a mapping between interfaces and classes, and it is your job to configure this mapping. You configure the mapping in your Blazor project’s Startup class, just like in ASP.NET Core. So open Startup.cs, as in Listing 4-12.
using Microsoft.AspNetCore.Blazor.Builder;
using Microsoft.Extensions.DependencyInjection;
namespace MyFirstBlazor.Client
{
  public class Startup
  {
    public void ConfigureServices(IServiceCollection services)
    {
      // Configure dependencies here
    }
    public void Configure(IBlazorApplicationBuilder app)
    {
      app.AddComponent<App>("app");
    }
  }
}
Listing 4-12

The Startup Class

See the comment? The idea is that you configure the mapping from the interface to the class here, and you use extension methods on the serviceProvider. Which extension method you call from Figure 4-3 depends on the lifetime you want to give the dependency. There are three options for the lifetime of an instance, which I will discuss next.
../images/469993_1_En_4_Chapter/469993_1_En_4_Fig3_HTML.jpg
Figure 4-3

Configuring dependency injection

Singleton Dependencies

Singleton classes are classes that only have one instance. They are typically used to manage some global state; for example, you could have a class that keeps track of how many times people have clicked a certain product. Having multiple instances of this class would complicate things because they would have to start communicating with each other to keep tracks of the clicks. Singleton classes can also be classes that don’t have any state, that only have behavior (utility classes such as one that does conversions between imperial and metric units). You configure dependency injection to reuse the same instance all the time with the AddSingleton extension method, as in Listing 4-13.
public void ConfigureServices(IServiceCollection services)
{
  services.AddSingleton<IProductsService, ProductsService>();
}
Listing 4-13

Adding a Singleton to Dependency Injection

Why not use static methods instead of singletons? Static methods and properties are very hard to replace with fake implementations during testing. (Have you ever tried to test a method that uses a date with DateTime.Now, and you want to test it with February 29 of some quantum leap year?) However, during testing you can easily replace the real class with a fake class because it implements an interface!

Transient Dependencies

When you configure dependency injection to use a transient class, each time an instance needs to be created by the IoCC it will create a fresh instance. The IoCC will also Dispose of the instance (when your class implements the IDisposable interface) when it is no longer needed. Most server-side classes should be transient because each request on a server should not depend on previous requests.

However, in Blazor you are working client side, and in that case the UI stays put for the entire interaction. This means that you will have components that only have one created instance and only one instance of the dependency. You might think in this case transient and singleton will do the same thing. But there can be another component that needs the same type of dependency. If you are using a singleton, then both components will share the same instance of the dependency, while with transient each gets their own instance! You should be aware of this.

You configure dependency injection to use transient instances with the AddTransient extension method, as in Listing 4-14.
public void ConfigureServices(IServiceCollection services)
{
  services.AddTransient<IProductsService, ProductsService>();
}
Listing 4-14

Adding a Transient Class to Dependency Injection

Scoped Dependencies

When you configure dependency injection to use a scoped dependency, the IoCC will reuse the same instance per request but will use new instances between different requests. This is especially useful if you use repository objects. Repository objects keep track of all changes made to its objects and then allow you to save (or discard) all changes at the end of the request. If you use transient instancing for repositories, a single request might lose some changes, which would result in subtle bugs. Let’s look at an example. Imagine you have a DebitService and another CreditService. Both make changes to a bank account and both use a BankRepository object as a dependency. A TransferService uses a DebitService to debit one account, and the CreditService credits an account, all using the BankRepository. Look at Listing 4-15.
public class TransferService {
  private DebitService ds;
  private CreditService cs;
  private BankRepository br;
  public TransferService(
    DebitService ds, CreditService cs, BankRepository br)
  {
    this.ds = ds;
    this.cs = cs;
    this.br = br;
  }
  public Transfer(decimal amount, Account from, Account to) {
    ds.Debit(from, amount);
    cs.Credit(to, amount);
    br.Commit();
  }
}
Listing 4-15

Implementing a TransferService

If all three services use the same instance of BankRepository, then this should work fine, as in Figure 4-4.
../images/469993_1_En_4_Chapter/469993_1_En_4_Fig4_HTML.jpg
Figure 4-4

Using a scoped repository

But if each receives their own new instance of BankRepository, the Commit method will do nothing because no changes were made to the BankRepository instance of the TransferService, as in Figure 4-5.
../images/469993_1_En_4_Chapter/469993_1_En_4_Fig5_HTML.jpg
Figure 4-5

Using a transient repository

Using scoped dependencies in Blazor will generally be of no practical use, but in the next chapter you will use a scoped instance to implement the microservice.

Never use scoped dependencies inside singletons. The scoped dependency will probably have an incorrect state after the first request.

Disposing Dependencies

One of the nice extras you get with dependency injection is that it takes care of calling the Dispose method of instances that implement IDisposable. If the BankRepository class of the previous example implements IDisposable, cleanup will occur at the end of the lifetime of the instance. In the case of a singleton, this would be at the end of the program; for scoped instances, this would be at the end of the request; and for transient instances, this would normally be when your component is removed from the UI. In general, if your classes implement IDisposable correctly, you don’t have to take care of anything else.

Building Blazor Services

Let’s go back to your PizzaPlace project and introduce it to some services. I can think of at least two services: one to retrieve the menu and one to place the order when the user clicks the Order button.

Start by reviewing the Index component, which is shown in Listing 4-16 with the methods left out for conciseness.
@page "/"
<!-- Menu -->
<PizzaList Title="Our selected list of pizzas"
           Menu="@State.Menu"
           Selected="@((pizza) => AddToBasket(pizza))" />
<!-- End menu -->
<!-- Shopping Basket -->
<ShoppingBasket Title="Your current order"
                Basket="@State.Basket"
                GetPizzaFromId="@State.Menu.GetPizza"
                Selected="@(pos => RemoveFromBasket(pos))" />
<!-- End shopping basket -->
<!-- Customer entry -->
<CustomerEntry Title="Please enter your details below"
               Customer="@State.Basket.Customer"
               Submit="@((_) => PlaceOrder())"/>
<!-- End customer entry -->
<p>@State.ToJson()</p>
@functions {
private State State { get; } = new State()
{
  Menu = new Menu
  {
    Pizzas = new List<Pizza> {
      new Pizza(1, "Pepperoni", 8.99M, Spiciness.Spicy ),
      new Pizza(2, "Margarita", 7.99M, Spiciness.None ),
      new Pizza(3, "Diabolo", 9.99M, Spiciness.Hot )
    }
  }
};
...
}
Listing 4-16

The Index Component

Pay special attention to the State property. You will initialize the State.Menu property from a service, and you will use dependency injection to pass the service.

Adding the MenuService and IMenuService abstraction

If you are using Visual Studio, right-click the PizzaPlace.Shared project and select Add ➤ New Item. If you are using Code, right-click the PizzaPlace.Shared project and select Add File. Add a new interface class called IMenuService and complete it as shown in Listing 4-17.
using System.Threading.Tasks;
namespace PizzaPlace.Shared
{
  public interface IMenuService
  {
    Task<Menu> GetMenu();
  }
}
Listing 4-17

The IMenuService Interface

This interface allows you to retrieve a menu. Note that the GetMenu method returns a Task<Menu>; this is because you expect the service to retrieve your menu from a server (you will build this in following chapters) and you want the method to support an asynchronous call.

Let’s elaborate on this. Have a look at the OnInitAsync method from Listing 4-20. It is an asynchronous method using the async keyword in its declaration. Inside the OnInitAsync method you call the GetMenu method using the await keyword, which requires GetMenu to return a Task<Menu>. Thanks to the async/await syntax this is easy to do but it does require that you return a task.

Now add the HardCodedMenuService class to the PizzaPlace.Shared project, as in Listing 4-18.
using System.Collections.Generic;
using System.Threading.Tasks;
namespace PizzaPlace.Shared
{
  public class HardCodedMenuService : IMenuService
  {
    public Task<Menu> GetMenu()
    {
      return Task.FromResult<Menu>(new Menu {
        Pizzas = new List<Pizza> {
          new Pizza(1, "Pepperoni", 8.99M, Spiciness.Spicy ),
          new Pizza(2, "Margarita", 7.99M, Spiciness.None ),
          new Pizza(3, "Diabolo", 9.99M, Spiciness.Hot )
        }
      });
    }
  }
}
Listing 4-18

The HardCodedMenuService Class

Now you are ready to use the IMenuService in your Index component. Start by adding the dependency on IMenuService using the @inject syntax, as in Listing 4-19.
@page "/"
@using PizzaPlace.Shared;
@inject IMenuService menuService
<!-- Menu -->
...
Listing 4-19

Stating That the Index Component Depends on an IMenuService

You initialize the State.Menu property in the OnInitAsync lifecycle method, as in Listing 4-20. You already have an OnInit method from the previous chapter that you don’t need any more so don’t forget to remove it.
@functions {
  private State State { get; } = new State();
  protected override async Task OnInitAsync()
  {
    State.Menu = await menuService.GetMenu();
    this.State.Basket.Customer.PropertyChanged +=
      (sender, e) => this.StateHasChanged();  }
...
}
Listing 4-20

Initializing the Index Component’s Menu

Never call asynchronous services in your Blazor component’s constructor; always use OnInitAsync or OnParametersSetAsync.

Now you are ready to configure dependency injection, so open Startup.cs from the client project. You’ll use a transient object, as stated in Listing 4-21.
using Microsoft.AspNetCore.Blazor.Builder;
using Microsoft.Extensions.DependencyInjection;
using PizzaPlace.Shared;
namespace PizzaPlace.Client
{
  public class Startup
  {
    public void ConfigureServices(IServiceCollection services)
    {
      services.AddTransient<IMenuService,
                            HardCodedMenuService>();
    }
    public void Configure(IBlazorApplicationBuilder app)
    {
      app.AddComponent<App>("app");
    }
  }
}
Listing 4-21

Configuring Dependency Injection for the MenuService

Run your Blazor project. Everything should still work!

Ordering Pizzas with a Service

When the user makes a selection of pizzas and fills in the customer information, you want to send the order to the server so they can warm up the oven and send some nice pizzas to the customer’s address. Start by adding an IOrderService interface to the PizzaPlace.Shared project as in Listing 4-22.
using System.Threading.Tasks;
namespace PizzaPlace.Shared
{
  public interface IOrderService
  {
    Task PlaceOrder(Basket basket);
  }
}
Listing 4-22

The IOrderService Abstraction as a C# Interface

To place an order, you just send the basket to the server. In the next chapter, you will build the actual server-side code to place an order; for now, you will use a fake implementation that simply writes the order to the browser’s console. Add a class called ConsoleOrderService to the PizzaPlace.Shared project as in Listing 4-23.
using System;
using System.Threading.Tasks;
namespace PizzaPlace.Shared
{
  public class ConsoleOrderService : IOrderService
  {
    public Task PlaceOrder(Basket basket)
    {
      Console.WriteLine($"Placing order for {basket.Customer.Name}");
      return Task.CompletedTask;
    }
  }
}
Listing 4-23

The ConsoleOrderService

The PlaceOrder method simply writes the basket to the console. However, this method implements the asynchronous pattern from .NET, so you need to return a Task instance. This is easily done using the Task.CompletedTask property. Task.CompletedTask is simply a “no nothing” task and is very handy if you need to implement a method that needs to return a Task instance.

Inject the IOrderService into the Index component as in Listing 4-24.
@page "/"
@using PizzaPlace.Shared;
@inject IMenuService  menuService
@inject IOrderService  orderService
Listing 4-24

Injecting the IOrderService

Use the order service when the user clicks on the Order button by replacing the implementation of the PlaceOrder method in the Index component. Since the orderService is asynchronous, you need to invoke it in an asynchronous way, as in Listing 4-25.
private async Task PlaceOrder()
{
  await orderService.PlaceOrder(State.Basket);
}
Listing 4-25

The Asynchronous PlaceOrder Method

As the final step, configure dependency injection. Again, make the orderService transient as in Listing 4-26.
using Microsoft.AspNetCore.Blazor.Builder;
using Microsoft.Extensions.DependencyInjection;
using PizzaPlace.Shared;
namespace PizzaPlace.Client
{
  public class Startup
  {
    public void ConfigureServices(IServiceCollection services)
    {
      services.AddTransient<IMenuService,
                            HardCodedMenuService>();
      services.AddTransient<IOrderService,
                            ConsoleOrderService>();
    }
    public void Configure(IBlazorApplicationBuilder app)
    {
      app.AddComponent<App>("app");
    }
  }
}
Listing 4-26

Configuring Dependency Injection for the orderService

Think about this. How hard will it be to replace the implementation of one of the services? There is only one place that says which class you will be using, and that is in Listing 4-26. In the next chapter, you will build the server-side code needed to store the menu and the orders, and in the chapter after that you will replace these services with the real deal!

Build your project. You will get a warning about making a call to an asynchronous method. This is because the IOrderService’s PlaceOrder method is now asynchronous. Fix it by changing the CustomerEntry’s Submit property to use an asynchronous lambda function as in Listing 4-27.
<!-- Customer entry -->
<CustomerEntry Title="Please enter your details below"
               Customer="@State.Basket.Customer"
               Submit="@(async (_) => await PlaceOrder())"/>
<!-- End customer entry -->
Listing 4-27

Changing to an Asynchronous Lambda Function

Build and run your project again, open your browser’s debugger, and open the console tab. Order some pizzas and click the Order button. You should see some feedback, as shown in Figure 4-6.
../images/469993_1_En_4_Chapter/469993_1_En_4_Fig6_HTML.jpg
Figure 4-6

The brower’s console showing that an order was placed

Summary

In this chapter you learned about dependency inversion, which is a best practice for building easily maintainable and testable object-oriented applications. You also saw that dependency injection makes it very easy to create objects with dependencies, especially objects that use dependency inversion. When you configure dependency injection, you need to be careful with the lifetime of your instances, so let’s repeat that again:
  • Transient objects are always different; a new instance is provided to every component and every service.

  • Scoped objects are the same within a request but different across different requests.

  • Singleton objects are the same for every object and every request.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.116.67.177