© Peter Himschoot 2020
P. HimschootMicrosoft Blazorhttps://doi.org/10.1007/978-1-4842-5928-3_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, we will discuss dependency inversion and injection and why it is a fundamental part of Blazor. We will illustrate this by building a Service that encapsulates where the data gets retrieved and stored.

What Is Dependency Inversion?

Currently, our Blazor PizzaPlace application 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 could be done in the component itself, but this is a bad idea. Why? Because technology changes quite often, and different customers for your application might want to use their specific technology, requiring you to update your app for every customer.

Instead, we will put this logic into a Service object . A Service object’s role is to encapsulate specific business rules and especially how data is communicated between the client and the server. A Service object is also a lot easier to test since we 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, and 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>
@code {
  private 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 replace it with another class. This is also known as tight coupling; see Figure 4-1.
../images/469993_2_En_4_Chapter/469993_2_En_4_Fig1_HTML.png
Figure 4-1

Traditional layered approach with tight coupling

Now you want to test the ProductList component, and ProductsService requires a server on the network to talk to. In this case, you will need to 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 you are using the ProductsService in several places in your location, and you need to replace it with another class. Now you will need to find every use of the ProductsService and replace the class. What a maintenance nightmare!

Using the Dependency Inversion Principle

The dependency inversion principle states
  1. 1.

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

     
  2. 2.

    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 the 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 would look like Listing 4-2.
public interface IProductsService
{
  IEnumerable<Product> GetAllProducts();
}
Listing 4-2

The abstraction as described in an interface

And we change the ProductsList component to rely on this abstraction, as 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>
@code {
  private IProductsService productsService;
}
Listing 4-3

The ProductList component using the IProductsService interface

Now the ProductList component (the high-level module in the preceding code) only relies on the IProductsService interface, an abstraction.

Of course, we now make the ProductsService (which is the low-level module) implement the interface as in Listing 4-4.
public class ProductsService : IProductsService
{
  public static List<Product> products = new List<Product>
  {
  };
  public IEnumerable<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 the ProductsService and run the test without needing a server, for example, in Listing 4-5. And if you are using the 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 tells 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 {
      Name = "Isabelle's Homemade Marmelade",
      Description = "...",
      UnitPrice = 1.99M
    }, new Product
    {
      Name = "Liesbeth's Applecake",
      Description = "...",
      UnitPrice = 3.99M
    }
  };
  public IEnumerable<Product> GetAllProducts()
  => products;
}
Listing 4-5

A hard-coded ProductsService used for testing

By applying the dependency inversion principle (see Figure 4-2), we gained a lot more flexibility.
../images/469993_2_En_4_Chapter/469993_2_En_4_Fig2_HTML.png
Figure 4-2

Loosely coupled objects through dependency inversion

Adding Dependency Injection

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

Passing the ProductsService in the constructor

But if the ProductsService also depends on another class, it quickly becomes like Listing 4-7.
new ProductList( new ProductsService(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, we 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, which specializes in creating objects for you. You simply ask it to create for you an instance of a type and it will take care of creating any dependencies it requires.

It is a little bit like in a movie where a surgeon, in the middle of an operation, needs a scalpel. The surgeon in the movie 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 constructor 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 that 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, for example, the third line of code in Listing 4-9.
@using MyFirstBlazor.Client.Services
@using MyFirstBlazor.Shared
@inject IProductsService productsService
<div>
  @foreach (var product in productsService.GetAllProducts())
  {
    <div>@product.Name</div>
    <div>@product.Description</div>
    <div>@product.UnitPrice</div>
  }
</div>
@code {
}
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 Microsoft.AspNetCore.Components;
using MyFirstBlazor.Shared;
namespace MyFirstBlazor.Client.Pages
{
  public partial class ProductList
  {
    [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.
<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 we need to discuss. When your dependency is a class, then the IoCC can easily know that it needs to create an instance of the class with the class’s 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 Program class. So open Program.cs, as in Listing 4-12.
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using Microsoft.Extensions.DependencyInjection;
using MyFirstBlazor.Shared;
using System.Threading.Tasks;
namespace MyFirstBlazor.Client
{
  public class Program
  {
    public static async Task Main(string[] args)
    {
      var builder =
        WebAssemblyHostBuilder.CreateDefault(args);
      builder.RootComponents.Add<App>("app");
      builder.Services.AddTransient(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
      await builder.Build().RunAsync();
    }
  }
}
Listing 4-12

The Program class

The idea is that you configure the mapping from the interface to the class here, and you use extension methods on builder.Services. 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 we will discuss next (ignore AddOptions which has to do with configuration).
../images/469993_2_En_4_Chapter/469993_2_En_4_Fig3_HTML.jpg
Figure 4-3

Configuring dependency injection

Singleton Dependencies

Singleton classes are classes that only have one instance (in the application’s scope). These 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 will have to start communicating with each other to keep track 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, for example, Listing 4-13.
builder.Services
       .AddSingleton<IProductsService,
                     HardCodedProductsService>();
Listing 4-13

Adding a singleton to dependency injection

Why not use static methods instead of singletons you say? 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?). 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 the previous request.

However, in Blazor we 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 that 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 transient each gets a unique 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.
builder.Services
       .AddTransient<IProductsService,
                     HardCodedProductsService>();
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 scope but uses new instances between different scopes. With ASP.NET Core running on the server, the scope normally corresponds to the current request. We will discuss the meaning of scope in Blazor a little later in this chapter. Using the server-side scope on the server is especially useful if you use repository objects. Repository objects keep track of all changes made to their objects and then allow you to save (or discard) all changes at the end of the scope/request. If you would 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 we 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_2_En_4_Chapter/469993_2_En_4_Fig4_HTML.jpg
Figure 4-4

Using a Scoped repository

But if each receives another 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_2_En_4_Chapter/469993_2_En_4_Fig5_HTML.jpg
Figure 4-5

Using a transient repository

Never use scoped dependencies inside singletons. Your singleton will have a reference to a scoped instance, which will be disposed of after the first request. The singleton does not get disposed, and when it uses the scoped instance, it will get an ObjectDisposedException because that is what an instance should do when you call its methods after it is disposed of. Should you forget, the runtime will throw a runtime error, to ensure this problem is found immediately!

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 that would be at the end of the program, for scoped instances 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.

Understanding Blazor Dependency Lifetime

Let’s look at the lifetime of the dependency injected dependencies in Blazor. For this, I have written an experiment which you can find in the included sources for this book.

The source code for this book is available on GitHub via the book’s product page, located at www.apress.com/9781484259276.

I started by building three services, each one with a different lifetime. For example, see Listing 4-16. Every time an instance gets created, it gets assigned a GUID. By displaying the instance’s GUID, it becomes easy to see which instance gets replaced with a new instance.
public class SingletonService : IDisposable
{
  public Guid Guid { get; set; }
  public SingletonService()
    => Guid = Guid.NewGuid();
  public virtual void Dispose()
    => Console.WriteLine($"{nameof(SingletonService)} disposed.");
}
Listing 4-16

One of the dependencies used for the experiment

Then I added these three services to the service collection, as in Listing 4-17.
using Microsoft.Extensions.DependencyInjection;
namespace BlazorLifetime.Shared
{
  public static class DependencyInjection
  {
    public static IServiceCollection AddLifetime(
      this IServiceCollection services)
    {
      services.AddSingleton<SingletonService>();
      services.AddTransient<TransientService>();
      services.AddScoped<ScopedService>();
      return services;
    }
  }
}
Listing 4-17

Adding the dependencies in dependency injection

And finally, I consume these services in a simple component in Listing 4-18, which I am using in two separate pages because this will allow me to switch from one to the other. This component will display GUIDs for each dependency.
<div>
  <h1>Singleton</h1>
  Guid: @singletonService.Guid
  <h1>Transient</h1>
  Guid: @transientService.Guid
  <h1>Scoped</h1>
  Guid: @scopedService.Guid
</div>
@inject SingletonService  singletonService
@inject TransientService transientService
@inject ScopedService scopedService
Listing 4-18

The component consuming the dependencies

Client-Side Blazor Experiment

Run the BlazorLifetime.Server project, which will start Blazor WebAssembly. We get Figure 4-6 on the first page (your GUIDs will be different). Switching to the other page shows Figure 4-7.
../images/469993_2_En_4_Chapter/469993_2_En_4_Fig6_HTML.jpg
Figure 4-6

Displaying Client-Side Blazor dependencies

../images/469993_2_En_4_Chapter/469993_2_En_4_Fig7_HTML.jpg
Figure 4-7

The dependencies from the other page

As you can see from the GUIDs, the Singleton instance gets reused all the time, which the transient instance gets replaced each time. In Blazor WebAssembly (a.k.a. Client-Side Blazor), the scope is set to the connection. And that is why a scoped instance always returns the same instance.

And what if we open another connection? Opening another tab in the browser with the same URL displays Figure 4-8 for the Home page and Figure 4-9 for the Other page.
../images/469993_2_En_4_Chapter/469993_2_En_4_Fig8_HTML.jpg
Figure 4-8

Opening another tab on the Home page

../images/469993_2_En_4_Chapter/469993_2_En_4_Fig9_HTML.jpg
Figure 4-9

And clicking the Other page

Since we have a new instance of the Blazor application running in the browser, we get a new instance for the singleton, and because the scope is the connection, we get another instance of the scoped instance. If you expected to see the same instance for the singleton in both tabs, please remember that here each tab holds another runtime of the Blazor application.

Server-Side Blazor Experiment

Now run the BlazorLifetime.ServerSideClient project. Your browser should open on the Home page as in Figure 4-10. Click the Other link to see Figure 4-11 (again with different GUIDs).
../images/469993_2_En_4_Chapter/469993_2_En_4_Fig10_HTML.jpg
Figure 4-10

Displaying Server-Side dependencies

../images/469993_2_En_4_Chapter/469993_2_En_4_Fig11_HTML.jpg
Figure 4-11

After clicking the Other link

Open another tab to see Figure 4-12 and click the Other link to see Figure 4-13.
../images/469993_2_En_4_Chapter/469993_2_En_4_Fig12_HTML.jpg
Figure 4-12

Opening another tab with Server-Side on the Home page

../images/469993_2_En_4_Chapter/469993_2_En_4_Fig13_HTML.jpg
Figure 4-13

Switching to the Other page

The Result of the Experiment

Now the experiment is complete; let us draw some conclusions about the lifetime of the injected dependencies. Every time an instance gets created, it gets a new GUID. This makes it easy to see if a new instance gets created or the same instance gets reused.

Transient Lifetime

This one is easy. Transient lifetime means you get a new instance every time.

Singleton Lifetime

With Blazor, we have server-side and client-side. With server-side Blazor, a Singleton gets shared by everyone on that server because it is scoped to the server instance. Everybody gets the same instance.

But with client-side Blazor, the same instance (of Singleton) is scoped to your browser's tab because the Singleton is scoped to the WASM application! Open two tabs in the same browser, and you get two instances of that Singleton. If you need a Singleton with CSB, you need to use a server-side singleton and use REST calls.

Scoped Lifetime

Blazor documentation clearly states that with client-side Blazor a Scoped instance is the same as a Singleton instance because the scope is the application. This is what we see in this experiment. Please use Scoped instances if you need the instance to correspond to the user (actually that user’s connection, you get a different connection with each tab).

For service-side Blazor, the Scoped instance is linked to the connection of the user. If the same user opens two tabs in the same browser, each tab receives its own instance of the Scoped instance.

For both CSB and SSB, if you need to have the same instance, no matter which tab the user is using, you cannot rely on dependency injection to do this for you. You will need to do some state handling yourself!

Building Pizza Services

Let’s go back to our 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 Listing 4-19 with the methods left out for conciseness.
@page "/"
<!-- Menu -->
<PizzaList Title="Our selection of pizzas"
           Menu="@State.Menu"
           Selected="@( async (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"
               ButtonTitle="Checkout"
               ButtonClass="btn btn-primary"
               @bind-Customer="@State.Basket.Customer"
               Submit="@PlaceOrder"
               />
<!-- End customer entry -->
<p>@State.ToJson()</p>
@code {
  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-19

The Index component

Pay special attention to the State property. We will initialize the State.Menu property from the MenuService service, and we 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 AddNew Item. If you are using Code, right-click the PizzaPlace.Shared project and select Add File. Add a new interface class IMenuService and complete it as in Listing 4-20.
using System.Threading.Tasks;
namespace PizzaPlace.Shared
{
  public interface IMenuService
  {
    Task<Menu> GetMenu();
  }
}
Listing 4-20

The IMenuService interface

This interface allows us to retrieve a menu. Do note that the GetMenu method returns a Task<Menu>; that is because we expect the service to retrieve our menu from a server (we will build this in the following chapters) and we want the method to support an asynchronous call. Let’s elaborate on this. Have a look at the OnInitializedAsync method from Listing 4-23. This is an asynchronous method using the async keyword in its declaration. Inside the OnInitializedAsync method, we 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-21.
using System.Collections.Generic;
using System.Threading.Tasks;
namespace PizzaPlace.Shared
{
  public class HardCodedMenuService : IMenuService
  {
    public Task<Menu> GetMenu()
      => Task.FromResult(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-21

The HardCodedMenuService class

Now we are ready to use the IMenuService in our Index component. Start by adding the dependency on IMenuService using the @inject syntax, as in Listing 4-22.
...
@inject IMenuService menuService
@code {
Listing 4-22

Stating that the Index component depends on an IMenuService

We initialize the State.Menu property in the OnInitializedAsync life cycle method, as in Listing 4-23.
@inject IMenuService menuService
@code {
    private State State { get; } = new State();
    protected override async Task OnInitializedAsync()
    {
      State.Menu = await menuService.GetMenu();
    }
    ...
}
Listing 4-23

Initializing the Index component’s Menu

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

Now we are ready to configure dependency injection, so open Program.cs from the client project. We’ll use a transient object as stated in Listing 4-24.
using Microsoft.AspNetCore.Blazor.Hosting;
using Microsoft.Extensions.DependencyInjection;
using PizzaPlace.Shared;
using System.Threading.Tasks;
namespace PizzaPlace.Client
{
  public class Program
  {
    public static async Task Main(string[] args)
    {
      var builder = WebAssemblyHostBuilder
        .CreateDefault(args);
      builder.RootComponents.Add<App>("app");
      builder.Services.AddTransient<IMenuService,
                                    HardCodedMenuService>();
      await builder.Build().RunAsync();
    }
  }
}
Listing 4-24

Configuring dependency injection for the MenuService

Run your Blazor project. Everything should still work! In the next chapters, we will replace this with a service to retrieve everything from a database.

Ordering Pizzas with a Service

When the user makes a selection of pizzas and fulfills the customer information, we 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-25.
using System.Threading.Tasks;
namespace PizzaPlace.Shared
{
  public interface IOrderService
  {
    Task PlaceOrder(Basket basket);
  }
}
Listing 4-25

The IOrderService abstraction as a C# interface

To place an order, we just send the basket to the server. In the next chapter, we will build the actual server-side code to place an order; for now, we 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-26.
using System;
using System.Threading.Tasks;
namespace PizzaPlace.Shared
{
  public class ConsoleOrderService : IOrderService
  {
    public async Task PlaceOrder(Basket basket)
    {
      Console.WriteLine($"Placing order for {basket.Customer.Name}");
      await Task.CompletedTask;
    }
  }
}
Listing 4-26

The ConsoleOrderService

The PlaceOrder method simply writes the basket to the console. However, this method implements the asynchronous pattern from .NET, so we 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-27.
@inject IMenuService menuService
@inject IOrderService orderService
@code {
Listing 4-27

Injecting the IOrderService

And use the order service when the user clicks the Order button by replacing the implementation of the PlaceOrder method in the Index component. Since the orderService is asynchronous, we need to invoke it asynchronously, as in Listing 4-28.
private async Task PlaceOrder()
{
  await orderService.PlaceOrder(State.Basket);
}
Listing 4-28

The asynchronous PlaceOrder method

As the final step, configure dependency injection. Again, we will make the orderService transient as in Listing 4-29.
using Microsoft.AspNetCore.Blazor.Hosting;
using Microsoft.Extensions.DependencyInjection;
using PizzaPlace.Shared;
using System.Threading.Tasks;
namespace PizzaPlace.Client
{
  public class Program
  {
    public static async Task Main(string[] args)
    {
      var builder = WebAssemblyHostBuilder
        .CreateDefault(args);
      builder.RootComponents.Add<App>("app");
      builder.Services.AddTransient<IMenuService,
                                    HardCodedMenuService>();
      builder.Services.AddTransient<IOrderService,
                                    ConsoleOrderService>();
      await builder.Build().RunAsync();
    }
  }
}
Listing 4-29

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 we will be using, and that is in Listing 4-29. In the next chapter (Chapter 5), we will build the server-side code needed to store the menu and the orders, and in Chapter 6, we will replace these services with the real deal!

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 in Figure 4-14.
../images/469993_2_En_4_Chapter/469993_2_En_4_Fig14_HTML.jpg
Figure 4-14

The brower’s console showing an order was placed

Summary

In this chapter, we discussed dependency inversion, which is the best practice for building easily maintainable and testable object-oriented applications. We 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:
  • Transient objects are always different; a new instance is provided to every component and every service.

  • Scoped objects are the same for a user’s connection, but different across different users and connections.

  • 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
3.16.147.124