Understanding Services and Dependency Injection
Grasping Dependency Inversion
Consider a ProductList component that uses a service class, creating the service instance with the new operator as shown in Listing 5-1.
Listing 5-1 Component Using
ProductsService
@using Dependency.Inversion.Shared
@foreach (var product in productsService.GetProducts())
{
<div>@product.Name</div>
<div>@product.Description</div>
<div>@product.Price</div>
}
@code {
private ProductsService productsService = new ProductsService();
}
This component now has a direct dependency on ProductsService! This is known as tight coupling; see Figure 5-1.
Suppose you want to test the ProductList component, but ProductsService needs to communicate with a server over the network. In this case, you would need to set up a server to run tests. If the server isn't ready yet (the developer responsible hasn't built it), you can't test your component! Or if you use ProductsService in multiple locations in your application, you'd need to replace it with another class. You would then have to find all usages of ProductsService and replace them. What a nightmare for maintenance!
Applying Dependency Inversion Principle
The Dependency Inversion Principle states:
A. High-level modules should not depend on low-level modules. Both should depend on abstractions. B. Abstractions should not depend on details. Details should depend on abstractions.
This means the ProductList component (a high-level module) should not directly depend on ProductsService (a low-level module). Instead, it should depend on an abstraction. In C# terms, it should depend on an interface that describes what ProductsService should be able to do, rather than describing how it works.
The IProductsService interface is shown in Listing 5-2.
Listing 5-2 Abstraction Defined in Interface
public interface IProductsService
{
IEnumerable<Product> GetProducts();
}
We modify the ProductList component to depend on this abstraction, as shown in Listing 5-3. Note that we still need to assign an instance to the productService variable.
Listing 5-3 ProductList Component Using IProductsService Interface
@using Dependency.Inversion.Shared
@foreach (var product in productsService.GetProducts())
{
<div>@product.Name</div>
<div>@product.Description</div>
<div>@product.Price</div>
}
@code
{
private IProductsService productsService;
}
Now the ProductList component (previously a high-level module) only depends on the IProductsService interface, which is an abstraction. The abstraction does not reveal how we will implement the GetProducts method.
Of course, we make ProductsService (the low-level module) implement the IProductsService interface, as shown in Listing 5-4.
Listing 5-4 ProductsService Implements IProductsService Interface
public class ProductsService : IProductsService
{
public IEnumerable<Product> GetProducts()
=> ...
}
If you want to test the ProductList component implemented using dependency inversion, you can create a hardcoded version of IProductsService and run tests without needing a server, as shown in Listing 5-5.
Listing 5-5 Hardcoded IProductsService for Testing
public class HardCodedProductsService : IProductsService
{
public IEnumerable<Product> GetProducts()
{
yield return new Product
{
Name = "Isabelle's Homemade Marmelade",
Description = "...",
Price = 1.99M
};
yield return new Product
{
Name = "Liesbeth's Applecake",
Description = "...",
Price = 3.99M
};
}
}
If you use the IProductsService interface in different parts of your application (instead of the ProductsService class), you just need to build another class that implements IProductsService and tell your application to use that other class!
By applying the Dependency Inversion Principle (see Figure 5-2), we gain more flexibility.
Adding Dependency Injection
If you were to run this application, you would receive a NullReferenceException. Why? Because the ProductsList component in Listing 5-3 still needs an instance of a class implementing IProductsService! We could pass ProductsService to the constructor of the ProductList component, as shown in Listing 5-6.
Listing 5-6 Passing
ProductsServicevia Constructor
new ProductList(new ProductsService())
But if ProductsService also depends on another class, it quickly becomes like Example 5-7. This is certainly not a practical approach! Therefore, we will use an Inversion-of-Control Container (this name is not mine!).
Listing 5-7 Manually Creating Deep Dependency Chain
new ProductList( new ProductsService(new Dependency()))
Using an Inversion-of-Control Container
An Inversion-of-Control Container (IoCC) is simply another object that creates objects for you. You just ask it to create an instance of a type, and it handles creating any dependencies required.
It's a bit like a surgeon in a movie who needs a scalpel during surgery. The surgeon reaches out his hand and says, "Scalpel number five!" The assisting nurse (IoCC) simply hands the scalpel to the surgeon. The surgeon doesn't care where the scalpel came from or how it was made.
How does IoCC know what dependencies your components require? There are several ways, largely depending on the IoCC.
Constructor Dependency Injection
Classes requiring dependencies can simply declare their dependencies in their constructors. IoCC checks the constructor and instantiates dependencies before calling the constructor. If those dependencies themselves have dependencies, IoCC builds them too! For example, if ProductsService has a constructor that takes a Dependency type parameter, as shown in Listing 5-8, IoCC will create an instance of Dependency, then call ProductsService's constructor with that instance. ProductsService constructor then stores a reference to the dependency in a field, as shown in Listing 5-8. If ProductsService's constructor takes multiple parameters, IoCC passes an instance for each parameter. Constructor injection is commonly used for required dependencies.
Listing 5-8 Constructor with Parameters in ProductsService
public class ProductsService
{
private readonly Dependency dep;
public ProductsService(Dependency dep)
{
this.dep = dep;
}
}
Property Dependency Injection
If the class IoCC needs to build has properties indicating dependencies, these properties are filled by IoCC. How properties perform this task depends on IoCC (in .NET, there are several different IoCC frameworks; some use attributes on properties). However, in Blazor, you can let IoCC inject an instance using the @inject directive in your razor file, as shown in the second line of Listing 5-9.
Listing 5-9 Injecting Dependencies Using
@injectDirective
@using Dependency.Inversion.Shared
@inject IProductsService productsService
@foreach (var product in productsService.GetProducts())
{
<div>@product.Name</div>
<div>@product.Description</div>
<div>@product.Price</div>
}
@code
{
}
If you use code separation, you can add a property to your class and apply the [Inject] attribute, as shown in Listing 5-10. Since this listing uses nullable reference types, we need to specify a default value! Remove compiler warnings.
Listing 5-10 Property Injection Using
InjectAttribute
public partial class ProductList
{
[Inject]
public IProductsService ProductsService { get; set; }= default!;
}
Then you can use this property directly in your razor file, as shown in Listing 5-11.
Listing 5-11 Using Injected ProductService Property
@foreach (var product in productsService.GetProducts())
{
<div>@product.Name</div>
<div>@product.Description</div>
<div>@product.Price</div>
}
Configuring Dependency Injection
We also need to discuss one thing. When your dependency is a class, IoCC can easily know it needs to create an instance using the class's constructor. But if your dependency is an interface, and you've applied the Dependency Inversion Principle, it usually needs to know which class to instantiate. Without your help, it cannot know.
IoCC maintains a mapping between interfaces and classes, and configuring this mapping is your job. You can configure the mapping in the Program class of a Blazor WebAssembly project (and in the Startup class for Blazor Server). Open Program.cs, as shown in Listing 5-12.
Listing 5-12 Program Class
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Net.Http;
using System.Threading.Tasks;
namespace Dependency.Inversion.Client
{
public class Program
{
public static async Task Main(string[] args)
{
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
builder.Services.AddScoped(sp => new HttpClient
{
BaseAddress =new Uri(builder.HostEnvironment.BaseAddress)
});
await builder.Build().RunAsync();
}
}
}
The Program class creates a builder instance with a Services property of type IServiceCollection. That's exactly what we need to configure. If you're familiar with ASP.NET Core, this is the same type used in the ConfigureServices method of the Startup class.
To configure the mapping for IoCC, you use extension methods on the IServiceCollection instance. Which extension method you call depends on the lifetime you want to assign to the dependency. For instance lifetimes, we'll discuss three options below.
Singleton Dependencies
Singleton classes are classes with only one instance (within the application scope). These are often used for managing global state. For example, you might have a class to track how many times someone clicks on a product. Having multiple instances of this class would complicate things since they would have to communicate to track clicks. Singleton classes can also be ones with no state but only behavior (utility classes, such as ones that convert between imperial and metric units). In this case, multiple instances are fine but wasteful and make the garbage collector work harder.
You can configure dependency injection to always reuse the same instance using the AddSingleton extension method, as shown in Listing 5-13. Every time IoCC needs an instance of the IProductsService interface, it uses an instance of the ProductService class.
Listing 5-13 Adding Singleton to Dependency Injection
builder.Services
.AddSingleton<IProductsService, ProductsService>();
There's an overload available (Listing 5-14) that allows you to create the singleton instance yourself and tell IoCC to use that instance.
Listing 5-14 Creating Singleton Yourself
ProductsService productsService = new ProductsService();
builder.Services.AddSingleton<IProductsService>(productsService);
Listing 5-15 Adding Singleton to Dependency Injection
builder.Services.AddSingleton<ProductsService>();
Why not use static methods instead of what you call singleton? During testing, static methods and properties are hard to replace with fakes (have you ever tried testing a method using DateTime.Now and wanted to test it on February 29th of a leap year?). During testing, you can easily replace real classes with fake ones because they implement an interface!
Now let's introduce the difference between Blazor WebAssembly and Blazor Server. In Blazor WebAssembly, your application runs within a browser tab. You can even run multiple copies of the same Blazor application in different tabs (or even different browsers). Each tab will have its own singleton instance in that browser tab's memory. So you can't share state between tabs using singletons in Blazor WASM. When you refresh a tab, the application reinitializes with a new singleton instance.
In Blazor Server, the application runs on the server. So the singleton is actually shared among all users running the Blazor application on the same server! But even here, your application can be hosted on multiple servers, each with its own singleton!
Transient Dependencies
Transient means temporary existence. In .NET, many objects are transient, possibly not surviving even after a single method call. For example, when concatenating strings, intermediate strings are discarded almost immediately after creation. It makes sense to use transient objects when you don't want to be affected by the previous state of an object. Instead, you start fresh by creating a new instance.
When you configure dependency injection to use the transient lifecycle for a dependency, IoCC creates a new instance every time it needs one.
You can configure dependency injection to use transient instances via the AddTransient extension method, as shown in Listing 5-16.
Listing 5-16 Adding Transient Class to Dependency Injection
builder.Services
.AddTransient<IProductsService, ProductsService>();
However, in Blazor, we work on the client side, and in this context, the UI remains unchanged throughout interactions. This means your component will have only one created instance and one dependency instance. You might think that transients and singletons would behave the same way in this case. But consider another component needing the same type of dependency. If you use a singleton, both components will share the same dependency instance, whereas transients give each component a unique instance! You should be aware of this.
Scoped Dependencies
When you configure dependency injection to use scoped dependencies, IoCC reuses the same instance within a scope but uses a new instance across different scopes. But what does scope mean?
There is still a difference between Blazor WebAssembly and Blazor Server. In Blazor WebAssembly, the scope is the application itself (running in the browser). With Blazor WebAssembly, scoped instances have the same lifetime as singletons.
Blazor Server uses SignalR connections to track individual users' applications (kind of like sessions). This circuit spans HTTP requests but not the SignalR connection used with Blazor Server.
You can configure dependencies to use the scoped lifetime via the AddScoped extension method, as shown in Listing 5-17.
Listing 5-17 Registering a Class to Use Scoped Lifetime
builder.Services.AddScoped<IProductsService, ProductsService>();
builder.Services.AddScoped<ProductsService>();
Understanding Blazor Dependency Lifecycles
I started by building three services, each with a different lifecycle (determined by configuration through dependency injection). For example, see Listing 5-18. Each time an instance is created, a GUID is assigned to it. By displaying the instance's GUID, it's easy to see which instance gets replaced by a new one. These classes also implement IDisposable, so we can see when they are disposed by checking the browser's debugger console.
Listing 5-18 One of the Dependencies for Experimentation
using System;
namespace Blazor.LifeTime.Shared
{
public class SingletonService : IDisposable
{
public Guid Guid { get; set; } = Guid.NewGuid();
public void Dispose()
=> Console.WriteLine("ScopedService Disposed");
}
}
Then I added these three services to the service collection, as shown in Listing 5-19 (Blazor WASM) and Listing 5-20 (Blazor Server).
Listing 5-19 Adding Dependencies for Blazor WASM
using Blazor.LifeTime.Shared;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Net.Http;
using System.Threading.Tasks;
namespace Blazor.Wasm.LifeTime
{
public class Program
{
public static async Task Main(string[] args)
{
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
builder.Services.AddScoped(sp => new HttpClient
{
BaseAddress = new Uri(builder.HostEnvironment.BaseAddress)
});
builder.Services.AddSingleton<SingletonService>();
builder.Services.AddTransient<TransientService>();
builder.Services.AddScoped<ScopedService>();
await builder.Build().RunAsync();
}
}
}
Listing 5-20. Adding Dependencies for Blazor Server (excerpt)
public void ConfigureServices(IServiceCollection services)
{
services.AddRazorPages();
services.AddServerSideBlazor();
services.AddSingleton<WeatherForecastService>();
services.AddSingleton<SingletonService>();
services.AddTransient<TransientService>();
services.AddScoped<ScopedService>();
}
Finally, I used these services in the Index component in Listing 5-21. This displays the GUID for each dependency (remember to add the correct @using to _Imports.razor).
Listing 5-21 Component Consuming Dependencies
@page "/"
@inject SingletonService singletonService
@inject TransientService transientService
@inject ScopedService scopedService
<div>
<h1>Singleton</h1>
Guid: @singletonService.Guid
<h1>Transient</h1>
Guid: @transientService.Guid
<h1>Scoped</h1>
Guid: @scopedService.Guid
</div>
Blazor WebAssembly Experiment
Run the Blazor.Wasm.Lifetime project, which starts Blazor WebAssembly. On the first page, you see the figure below (your GUIDs will differ).
Switch to the counter page and return to see the figure below.
Every time the Index component is created, it requests dependency injection for instances of SingletonService, TransientService, and ScopedService. The SingletonService instance is reused consistently, as we see the same GUID. The TransientService instance is replaced each time (because we get different GUIDs each time). We also see the same instance of ScopedService. In Blazor WebAssembly, the scope of scoped instances defaults to the browser tab (the application); they behave like singletons, so there's no difference.
What if we open another tab? Since we're running a new copy of the Blazor application in another tab, we get a new singleton instance, and because the scope is tied to the connection, we get another instance of the scoped service. Remember that each tab contains another copy of the Blazor application.
When are our instances disposed? As long as your application is running, singleton and scoped instances will persist and won't be released. But what about transient instances? If you really need to dispose of a transient instance when the component is released, you need to implement the IDisposable interface on the component and manually call Dispose on the transient instance, as shown in Listing 5-22! Or use OwningComponentBase (later).
Listing 5-22 Implementing
IDisposableon Component
@page "/"
@inject SingletonService singletonService
@inject TransientService transientService
@inject ScopedService scopedService
@implements IDisposable
<div>
<h1>Singleton</h1>
Guid: @singletonService.Guid
<h1>Transient</h1>
Guid: @transientService.Guid
<h1>Scoped</h1>
Guid: @scopedService.Guid
</div>
@code {
public void Dispose()=> transientService.Dispose();
}
Blazor Server Experiment
Now run the Blazor.Server.LifeTime project; ensure you're using Kestrel rather than IIS to run the server. Your browser should open on the index page as shown.
Select the Counter page and return to the Index page to see the figure below (again, you'll have different GUIDs).
Here, we see similar behavior to Blazor WASM. But don't be fooled. It's different, and we can see this by opening another tab. You should see the same GUID for the singleton instance. Now, since we're running on the server, the server provides a singleton instance for all users. Open the page in another browser; again, you'll see the same GUID.
Using OwningComponentBase
What if you want a service instance that belongs to your component and is automatically disposed when the component is released? You can make your component create its own scope by inheriting from the OwningComponentBase class. Look at Listing 5-23, which is the OwningComponent you can find in the provided project. Here, we inherit from OwningComponentBase. The OwningComponentBase class doesn't use regular dependency injection but has a ScopedServices property of type IServiceProvider. Any scoped instance should be created through the GetService or GetRequiredService methods of ScopedServices. These instances now belong to the component's scope and are automatically disposed when the component is released.
Listing 5-23 Component Inheriting from
OwningComponentBase
@using Microsoft.Extensions.DependencyInjection
@inherits OwningComponentBase
<h1>OwningComponent</h1>
Guid: @scopedService.Guid
@code {
private ScopedService scopedService;
protected override void OnInitialized()
=> scopedService = ScopedServices.GetRequiredService<ScopedService>();
}
If you only need one scoped instance, you can also use the generic OwningComponentBase<T> base class, which has a Service property of type T holding the scoped instance of T. Listing 5-24 shows an example. If you need to create additional scoped instances, you can still use the ScopedServices property.
Listing 5-24 Using
OwningComponentBase<T>
@inherits OwningComponentBase<ScopedService>
<h1>OwningComponent2</h1>
Guid: @Service.Guid
Now add these two components to the Index component, as shown in Listing 5-25. You can choose between Blazor Server and Blazor WebAssembly.
Listing 5-25 Using Components Inheriting from
OwningComponentBase
@page "/"
@inject SingletonService singletonService
@inject TransientService transientService
@inject ScopedService scopedService
<div>
<h1>Singleton</h1>
Guid: @singletonService.Guid
<h1>Transient</h1>
Guid: @transientService.Guid
<h1>Scoped</h1>
Guid: @scopedService.Guid
<OwningComponent/>
<OwningComponent2/>
</div>
Run your project and make sure the console is open. Click the Counter component. The console should display the processed ScopedService instance. Also note that each instantiation of OwningComponent and OwningComponent2 receives a new ScopedService instance.
Note: Don't implement
IDisposableon components that inherit fromOwningComponentBase, as this will prevent automatic disposal of scoped instances!
Experiment Results
Now that the experiments are complete, let's draw conclusions about the lifecycles of injected dependencies. Each time an instance is created, it gets a new GUID. This makes it easy to see whether a new instance was created or the same instance was reused.
Transient lifetimes are straightforward. A transient lifecycle means you get a new instance every time. This is the same for both Blazor WASM and Blazor Server.
Singleton lifecycles mean that in Blazor WASM, you get one instance throughout the entire application lifetime. If you truly need to share an instance across all users and tabs, you need to place it on the server and access it via server calls. For Blazor Server, everyone uses the same instance. Be sure not to put any user-specific information in singletons, as this would leak to other users (bad!).
In Blazor WASM, the scoped lifecycle is the same as singleton. However, for Blazor Server, we need to be careful. Blazor Server uses SignalR connections (called circuits) between the browser and server, and scoped instances are tied to circuits. If you need specific scoped behavior for a component, you can derive from the OwningComponentBase class.
For both Blazor WebAssembly and Blazor Server, if you need the same instance regardless of which tab a user uses, you cannot rely on dependency injection to do this for you. You need to handle state yourself!
Building Pizza Services
Let's return to our PizzaPlace project and introduce some services. I can think of at least two services: one for retrieving the menu and another for placing orders when a user clicks the "Order" button. Currently, these services will be very simple, but later we'll use them to establish communication with the server.
Listing 5-26 The Index Component
@code {
private State State { get; } = new State();
protected override void OnInitialized()
{
State.Menu.Add(
new Pizza(1, "Pepperoni", 8.99M, Spiciness.Spicy));
State.Menu.Add(
new Pizza(2, "Margarita", 7.99M, Spiciness.None));
State.Menu.Add(
new Pizza(3, "Diabolo", 9.99M, Spiciness.Hot));
}
private void AddToBasket(Pizza pizza)
=> State.Basket.Add(pizza.Id);
private void RemoveFromBasket(int pos)
=> State.Basket.RemoveAt(pos);
private void PlaceOrder()
{
Console.WriteLine("Placing order");
}
}
Note the State property. We'll initialize the State.Menu property from a MenuService service (which we'll build next) and use dependency injection to pass the service.
Adding MenuService and IMenuService Abstraction
If you're using Visual Studio, right-click the PizzaPlace.Shared project and select Add ➤New Item. If you're using code, right-click the PizzaPlace.Shared project and select Add File. Add a new interface class IMenuService and implement it, as shown in Listing 5-27.
Listing 5-27 IMenuService Interface
using System.Threading.Tasks;
namespace PizzaPlace.Shared
{
public interface IMenuService
{
ValueTask<Menu> GetMenu();
}
}
This interface allows us to retrieve a menu. Notice that the GetMenu method returns a ValueTask<Menu>; this is because we want the service to retrieve our menu from a server (we'll build this in upcoming chapters) and support asynchronous calls.
Let's elaborate. First, update the OnInitializedAsync method of the Index component (don't forget the @inject at the top), as shown in Example 5-28. This is an asynchronous method declared with the async keyword.
Never call asynchronous services in a Blazor component constructor; always use OnInitializedAsync or OnParametersSetAsync.
In the OnInitializedAsync method, we call the GetMenu method using the await keyword, which requires GetMenu to return Task<Menu> or ValueTask<T>. But why ValueTask<T> instead of Task<T>? Because I don't know how someone will implement the GetMenu method. They might execute it synchronously, for example, by retrieving it from a cache, making Task<T> more expensive than ValueTask<T>. Additionally, ValueTask<T> is a value type, meaning in synchronous cases, it doesn't end up on the heap.
Listing 5-28 Using IMenuService
@page "/"
@inject IMenuService MenuService
<!-- Menu -->
<PizzaList Title="Our Selection of Pizzas"
Items="@State.Menu.Pizzas"
ButtonTitle="Order"
ButtonClass="btn btn-success pl-4 pr-4"
Selected="@AddToBasket" />
<!-- End menu -->
<!-- Shopping Basket -->
<ShoppingBasket Orders="@State.Basket.Orders"
GetPizzaFromId="@State.Menu.GetPizza"
Selected="@RemoveFromBasket" />
<!-- End shopping basket -->
<!-- Customer entry -->
<CustomerEntry Title="Please enter your details below"
@bind-Customer="@State.Basket.Customer"
ButtonTitle="Checkout"
ButtonClass="mx-auto w-25 btn btn-success"
ValidSubmit="PlaceOrder" />
<!-- End customer entry -->
@State.ToJson()
@code {
private State State { get; } = new State();
protected override async Task OnInitializedAsync()
{
Menu menu = await MenuService.GetMenu();
foreach(Pizza pizza in menu.Pizzas)
{
State.Menu.Add(pizza);
}
}
private void AddToBasket(Pizza pizza)
=> State.Basket.Add(pizza.Id);
private void RemoveFromBasket(int pos)
=> State.Basket.RemoveAt(pos);
private void PlaceOrder()
{
Console.WriteLine("Placing order");
}
}
We're not ready to run this application yet because we haven't configured dependency injection. But run it anyway! When you get an error message, check the browser's debugger console. You should see the following error:
Unhandled exception rendering component: Cannot provide a value for
property 'MenuService' on type 'PizzaPlace.Client.Pages.Index'. There is no
registered service of type 'PizzaPlace.Shared.IMenuService'.
Dependency injection cannot provide an instance for IMenuService. Of course not! We did implement this interface. Add a new HardCodedMenuService class to the PizzaPlace.Shared project, as shown in Listing 5-29. The GetMenu method returns a new ValueTask<Menu> containing three different pizzas.
Listing 5-29
HardCodedMenuServiceClass
using System.Collections.Generic;
using System.Threading.Tasks;
namespace PizzaPlace.Shared
{
public class HardCodedMenuService : IMenuService
{
public ValueTask<Menu> GetMenu()
=> new ValueTask<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)
}
});
}
}
Now we're ready to use IMenuService in our Index component. Open Program.cs from the client project. We'll use the transient object as described in Example 5-30.
Listing 5-30 Configuring Dependency Injection for MenuService
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using Microsoft.Extensions.DependencyInjection;
using PizzaPlace.Shared;
using System;
using System.Net.Http;
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.AddScoped(sp => new HttpClient
{
BaseAddress = new Uri(
builder.HostEnvironment.BaseAddress)
});
builder.Services
.AddTransient<IMenuService, HardCodedMenuService>();
await builder.Build().RunAsync();
}
}
}
Run the Blazor project. Everything should still work! In the next chapters, we'll replace it with a service to retrieve everything from the database on the server.
Ordering Pizza Through Services
When users select pizza and fill in customer information, we want to send the order to the server so they can preheat the oven and deliver delicious pizzas to the customer's address. First, add an IOrderService interface to PizzaPlace. The shared project is shown in Listing 5-31.
Listing 5-31
IOrderServiceAbstract as C# Interface
using System.Threading.Tasks;
namespace PizzaPlace.Shared
{
public interface IOrderService
{
ValueTask PlaceOrder(ShoppingBasket basket);
}
}
To place an order, we simply send the shopping basket to the server. In the next chapter, we'll build the actual server-side code to place orders; for now, we'll use a fake implementation that just writes the order to the browser's console. Add a class named ConsoleOrderService to the PizzaPlace.Shared project, as shown in Listing 5-32.
Listing 5-32 ConsoleOrderService
using System;
using System.Threading.Tasks;
namespace PizzaPlace.Shared
{
public class ConsoleOrderService : IOrderService
{
public ValueTask PlaceOrder(ShoppingBasket basket)
{
Console.WriteLine($"Placing order for {basket.Customer.Name}");
return new ValueTask();
}
}
}
The PlaceOrder method simply writes the basket to the console. However, this method implements the asynchronous pattern in .NET, so we need to return a new ValueTask instance. Inject IOrderService into the Index component, as shown in Listing 5-33.
Listing 5-33 Injecting
IOrderService
@page "/"
@inject IMenuService MenuService
@inject IOrderService orderService
And replace the implementation of the PlaceOrder method in the Index component to use the order service when the user clicks the Order button. Since orderService returns a ValueTask (similar to Task), we need to call it using the await syntax, as shown in Listing 5-34.
Listing 5-34 Async
PlaceOrderMethod
private async Task PlaceOrder()
{
await orderService.PlaceOrder(State.Basket);
}
Final step, configure dependency injection. Similarly, we'll set IOrderService to transient, as shown in Listing 5-35.
Listing 5-35 Configuring Dependency Injection for
OrderService
builder.Services
.AddTransient<IMenuService, HardCodedMenuService>();
builder.Services
.AddTransient<IOrderService, ConsoleOrderService>();
Think about this. How difficult is it to replace one of these services? There's only one place where we specify which class to use, that is in Program (or Startup with Blazor Server). In future chapters, we'll build the server-side code needed to store menus and orders, and in subsequent chapters, we'll replace these services with real implementations!
Build and run your project again, open the browser's debugger, and go to the console tab. Order some pizzas, then click the order button. You should see some feedback written to the console.