Loading

第五章-服务和依赖注入

了解依赖倒置

想象一个使用服务类的 ProductList 组件,该组件使用 new 运算符创建服务,如清单 5-1 所示。

清单 5-1 使用 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();
}

该组件现在完全依赖于 ProductsService! 这称为紧耦合; 见图 5-1。

image

现在您要测试 ProductList 组件,而 ProductsService 需要网络上的服务器与之通信。 在这种情况下,您需要设置一个服务器来运行测试。 如果服务器还没有准备好(负责服务器的开发人员还没有找到它),你就不能测试你的组件! 或者您在您所在位置的多个地方使用 ProductsService,您需要将其替换为另一个类。 现在您将需要找到 ProductsService 的所有用途并替换该类。 多么可怕的维护噩梦!

使用依赖倒置原则

依赖倒置原则指出:

A. 高级模块不应该依赖于低级模块。 两者都应该依赖于抽象。
B. 抽象不应该依赖于细节。 细节应该取决于抽象。

这意味着 ProductsList 组件(较高级别的模块)不应直接依赖于 ProductsService(较低级别的模块)。 相反,它应该依赖于抽象。 使用 C# 术语:它应该依赖于描述 ProductsService 应该能够做什么的接口,而不是描述它应该如何工作的类。

IProductsService 接口如清单 5-2 所示。

清单 5-2 接口中描述的抽象

public interface IProductsService
{
    IEnumerable<Product> GetProducts();
}

我们将 ProductsList 组件更改为依赖此抽象,如清单 5-3 所示。 请注意,我们仍然需要为 productService 变量分配一个实例。

清单 5-3 使用 IProductsService 接口的 ProductList 组件

@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;
}

现在 ProductList 组件(之前的高级模块)只依赖于 IProductsService 接口,这是一种抽象。 抽象并没有透露我们将如何实现 GetProducts 方法。

当然,现在我们让 ProductsService(它是低级模块)实现 IProductsService 接口,如清单 5-4 所示。

清单 5-4 ProductsService 实现 IProductsService 接口

public class ProductsService : IProductsService
{
    public IEnumerable<Product> GetProducts()
    => ...
}

如果您想测试使用依赖反转实现的 ProductList 组件,您可以构建 IProductsService 的硬编码版本并在不需要服务器的情况下运行测试,例如清单 5-5。

清单 5-5 用于测试的硬编码 IProductsService

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
        };
    }
}

如果您在应用程序的不同位置使用 IProductsService 接口(而不是 ProductsService 类),您只需构建另一个实现 IProductsService 接口的类并告诉您的应用程序使用另一个类即可!

通过应用依赖倒置原则(见图 5-2),我们获得了更多的灵活性。

image

添加依赖注入

如果您要运行此应用程序,您将收到 NullReferenceException。 为什么? 因为清单 5-3 中的 ProductsList 组件仍然需要一个实现 IProductsService 的类的实例! 我们可以在 ProductList 组件的构造函数中传递 ProductsService,例如清单 5-6。

清单 5-6 在构造函数中传递 ProductsService

new ProductList(new ProductsService())

但如果 ProductsService 还依赖于另一个类,它很快就会变成如示例 5-7 所示。 这当然不是一种实用的工作方式! 因此,我们将使用 Inversion-of-Control Container(这个名字不是我发明的!)。

清单 5-7 手动创建深层依赖链

new ProductList( new ProductsService(new Dependency()))

使用控制反转容器

控制反转容器 (IoCC) 只是另一个对象,它专门为您创建对象。 您只需要求它为您创建一个类型的实例,它就会负责创建它所需的任何依赖项。

这有点像电影中的外科医生,在手术过程中,需要一把手术刀。 电影中的外科医生伸出他(或她)的手,要求“5 号手术刀!”。 正在协助的护士(控制反转容器)只是将手术刀递给外科医生。 外科医生并不关心手术刀来自哪里或它是如何制造的。

那么,IoCC 怎么知道你的组件需要哪些依赖呢? 有几种方法,很大程度上取决于 IoCC。

构造函数依赖注入

需要依赖的类可以简单地在其构造函数中声明它们的依赖关系。IoCC 将检查构造函数并在调用构造函数之前实例化依赖关系。 如果这些依赖有自己的依赖,那么 IoCC 也会构建它们! 例如,如果 ProductsService 有一个接受 Dependency 类型参数的构造函数,如清单 5-8 所示,那么 IoCC 将创建一个 Dependency 类型的实例,然后使用该实例调用 ProductsService 的构造函数。 ProductsService 构造函数然后在某个字段中存储对依赖项的引用,如清单 5-8 所示。 如果 ProductsService 的构造函数采用多个参数,那么 IoCC 将为每个参数传递一个实例。 构造函数注入通常用于所需的依赖项。

清单 5-8 ProductsService 的带参数的构造函数

public class ProductsService
{
    private readonly Dependency dep;
    public ProductsService(Dependency dep)
    {
        this.dep = dep;
    }
}

属性依赖注入

如果 IoCC 需要构建的类具有指示依赖关系的属性,则这些属性由 IoCC 填充。 属性执行此操作的方式取决于 IoCC(在 .NET 中,有几个不同的 IoCC 框架;其中一些使用属性上的属性),但在 Blazor 中,您可以让 IoCC 使用 @ 注入一个实例 在你的 razor 文件中注入指令,例如清单 5-9 中的第二行代码。

清单 5-9。 使用 @inject 指令注入依赖项

@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
{
}

如果您使用代码分离,您可以向您的类添加一个属性并应用 [Inject] 属性,如清单 5-10 所示。 由于此清单使用可空引用类型,我们需要指定一个默认值! 删除编译器警告。

清单 5-10 使用 Inject 属性进行属性注入

public partial class ProductList
{
    [Inject]
    public IProductsService ProductsService { get; set; }= default!;
}

然后你可以直接在你的 razor 文件中使用这个属性,如清单 5-11 所示。

清单 5-11 使用依赖注入的产品服务属性

@foreach (var product in productsService.GetProducts())
{
    <div>@product.Name</div>
    <div>@product.Description</div>
    <div>@product.Price</div>
}

配置依赖注入

我们还需要讨论一件事。 当您的依赖项是一个类时,IoCC 可以很容易地知道它需要使用类的构造函数创建该类的实例。 但是如果你的依赖是一个接口,如果你应用依赖倒置的原则,它通常需要它,那么它使用哪个类来创建实例? 没有你的帮助,它无法知道。

IoCC 具有接口和类之间的映射,配置此映射是您的工作。 您可以在 Blazor WebAssembly 项目的 Program 类(以及 Blazor Server 的 Startup 类)中配置映射。 所以打开 Program.cs,如清单 5-12 所示。

清单 5-12 Program 类

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();
        }
    }
}

Program 类创建一个构建器实例,该实例具有 IServiceCollection 类型的属性 Services。 我们需要配置的正是这个 IServiceCollection。 如果您熟悉 ASP.NET Core,则它与 Startup 类的 ConfigureServices 方法中使用的类型相同。

要为 IoCC 配置映射,您可以在 IServiceCollection 实例上使用扩展方法。 您调用哪种扩展方法取决于您要赋予依赖项的生命周期。 对于实例的生命周期,我们将在下面讨论三个选项。

Singleton依赖

单例类是只有一个实例(在应用程序范围内)的类。这些通常用于管理一些全局状态。 例如,您可以有一个类来跟踪人们点击某个产品的次数。 拥有此类的多个实例会使事情变得复杂,因为它们必须开始相互通信以跟踪点击。 单例类也可以是没有任何状态而只有行为的类(实用程序类,例如在英制和公制单位之间进行转换的类)。 在这种情况下,您可以有多个实例,但这只是浪费并且会使垃圾收集器更加努力地工作。

您可以使用 AddSingleton 扩展方法配置依赖注入以始终重用同一个实例,例如清单 5-13。 每次 IoCC 需要 IProductsService 接口的实例时,它都会使用 ProductService 类的实例。

清单 5-13 将单例添加到依赖注入

builder.Services
.AddSingleton<IProductsService, ProductsService>();

有一个可用的重载(清单 5-14)允许您自己创建单例实例,然后告诉 IoCC 使用该实例。

清单 5-14 自己创建单例

ProductsService productsService = new ProductsService();
builder.Services.AddSingleton<IProductsService>(productsService);

清单 5-15 将单例添加到依赖注入

builder.Services.AddSingleton<ProductsService>();

为什么不使用静态方法而不是你说的单例? 在测试期间,静态方法和属性很难用假实现替换(您是否曾经尝试过使用 DateTime.Now 来测试使用日期的方法,并且您想在某个闰年的 2 月 29 日对其进行测试?)。 在测试期间,您可以轻松地将真实类替换为假类,因为它实现了一个接口!

现在介绍 Blazor WebAssembly 和 Blazor Server 之间的区别。 在 Blazor WebAssembly 中,您的应用程序在浏览器的选项卡中运行。 您甚至可以在浏览器的不同选项卡(甚至是不同的浏览器)中运行同一 Blazor 应用程序的多个副本。 每个选项卡都将在该浏览器选项卡的内存中拥有自己的单例实例。 因此,您不能使用单例在 Blazor WASM 的选项卡之间共享状态。 当您刷新选项卡时,应用程序将使用单例的新实例重新初始化。

使用 Blazor Server,应用程序在服务器上运行。 所以这里的单例实际上是在同一台服务器上运行 Blazor 应用程序的每个用户之间共享的! 但是即使在这里,您的应用程序也可以托管在多个服务器上,并且每个服务器都有自己的单例!

Transient依赖

瞬态意味着短暂的存在。 在 .NET 中,有很多对象是短暂的,它们甚至可能在单个方法调用之后无法生存。 例如,当您连接几个字符串时,中间字符串在创建后几乎立即被丢弃。 当您不想受到对象先前状态的影响时,使用瞬态对象很有意义。 相反,您可以通过创建一个新实例来重新开始。

当您将依赖注入配置为使用类的瞬态生命周期时,每次 IoCC 需要一个实例时,它都会创建一个新实例。

您可以通过 AddTransient 扩展方法配置依赖注入以使用瞬态实例,如清单 5-16 所示。

清单 5-16 将瞬态类添加到依赖注入

builder.Services
.AddTransient<IProductsService, ProductsService>();

但是,在 Blazor 中,我们在客户端工作,在这种情况下,UI 在整个交互过程中保持不变。 这意味着您将拥有只有一个已创建实例和一个依赖项实例的组件。 你可能会认为在这种情况下瞬态和单例会做同样的事情。 但是可能有另一个组件需要相同类型的依赖。 如果您使用的是单例,那么两个组件将共享同一个依赖实例,而瞬态每个组件都将获得一个唯一的实例! 你应该意识到这一点。

Scoped依赖

当您将依赖注入配置为使用作用域依赖时,IoCC 将在每个作用域重用相同的实例,但在不同作用域之间使用新实例。 但是范围是什么意思?

Blazor WASM 和 Blazor Server 之间还是有区别的。 在 Blazor WASM 中,范围是应用程序(在浏览器中运行)本身。 使用 Blazor WASM,作用域实例将具有与单例相同的生命周期。

Blazor Server 使用 SignalR 连接电路来跟踪单个用户的应用程序(有点像会话)。 此电路跨越 HTTP 请求,但不跨越与 Blazor 服务器一起使用的 SignalR 连接

您可以通过 AddScoped 扩展方法配置依赖项以使用作用域生命周期,如清单 5-17 所示。

清单 5-17 注册一个类以使用作用域生命周期

builder.Services.AddScoped<IProductsService, ProductsService>();
builder.Services.AddScoped<ProductsService>();

了解 Blazor 依赖关系生命周期

我从构建三个服务开始,每个服务都有不同的生命周期(通过依赖注入的配置确定)。 例如,见清单 5-18。 每次创建实例时,都会为其分配一个 GUID。 通过显示实例的 GUID,很容易看到哪个实例被新实例替换。 这些类还实现了 IDisposable,因此我们可以通过查看浏览器的调试器控制台来查看它们何时被释放。

清单 5-18 用于实验的依赖项之一

using System;
namespace Blazor.LifeTime.Shared
{
    public class SingletonService : IDisposable
    {
        public Guid Guid { get; set; } = Guid.NewGuid();
        public void Dispose()
            => Console.WriteLine("ScopedService Disposed");
    }
}

然后我将这三个服务添加到服务集合中,如清单 5-19(Blazor WASM)和清单 5-20(Blazor Server)。

清单 5-19 添加 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();
        }
    }
}

清单 5-20。 添加 Blazor 服务器的依赖项(摘录)

public void ConfigureServices(IServiceCollection services)
{
    services.AddRazorPages();
    services.AddServerSideBlazor();
    services.AddSingleton<WeatherForecastService>();
    services.AddSingleton<SingletonService>();
    services.AddTransient<TransientService>();
    services.AddScoped<ScopedService>();
}

最后,我在清单 5-21 中的 Index 组件中使用这些服务。这将显示每个依赖项的 GUID(不要忘记将正确的 @using 添加到 _Imports.razor)。

清单 5-21 Consuming 依赖的组件

@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 实验

运行 Blazor.Wasm.Lifetime 项目,该项目将启动 Blazor WebAssembly。 我们在第一页看到下图 (您的 GUID 会有所不同)。

image

切换到计数器页面并返回显示下图 。

image

每次创建 Index 组件时,它都会为 SingletonServiceTransientServiceScopedService 的实例请求依赖注入。 SingletonService 实例一直被重用,因为我们看到了相同的 GUID。 TransientService 实例每次都会被替换(因为每次我们得到不同的 GUID)。 我们还看到 ScopedService 的相同实例。 在 Blazor WebAssembly 中,作用域实例的作用域默认为浏览器的选项卡(应用程序); 他们表现得像单身人士,所以没有区别。

如果我们打开另一个标签怎么办? 由于我们在另一个选项卡中运行了 Blazor 应用程序的新副本,因此我们获得了单例的新实例,并且因为作用域是连接,所以我们获得了作用域实例的另一个实例。 如果您希望在两个选项卡中看到相同的单例实例,请记住这里每个选项卡都包含 Blazor 应用程序的另一个副本。

我们的实例何时被处置? 只要您的应用程序正在运行,单例实例和作用域实例都将存在,因此它们不会被释放。 但是瞬态实例呢? 如果你真的需要在组件被释放时释放一个瞬态实例,你需要在组件上实现如清单 5-22 所示的 IDisposable 接口,并自己在瞬态实例上调用 Dispose! 或者使用 OwningComponentBase(稍后)。

清单 5-22 在组件上实现 IDisposable

@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 服务器实验

现在运行 Blazor.Server.LifeTime 项目; 确保您使用 Kestrel 而不是 IIS 运行服务器。 您的浏览器应该会在索引页面上打开,如图所示。

image

选择 Counter 页面并返回 Index 页面以查看下图(同样,您将有不同的 GUID)。

image

在这里,我们看到了与 Blazor WASM 类似的行为。 但不要上当。 这不一样,我们可以通过打开另一个选项卡来看到。 您应该会看到与下图 中的单例实例相同的 GUID。 现在我们在服务器上运行,服务器将为所有用户提供一个单例实例。 在另一个浏览器中打开页面; 再次,您将看到相同的 GUID。

image

使用 OwneringComponentBase

如果您想要一个属于您的组件的服务实例,并且希望在组件被释放时自动释放该实例,该怎么办? 您可以通过从 OwningComponentBase 类派生来使您的组件创建自己的范围。 查看清单 5-23,它是您可以在提供的项目中找到的 OwningComponent。 在这里,我们继承自 OwningComponentBaseOwningComponentBase 类没有使用常规依赖注入,而是具有 IServiceProviderScopedServices 属性。 任何作用域实例都应通过 ScopedServicesGetServiceGetRequiredService 方法创建。 这些实例现在属于组件的范围,并且会在释放组件时自动释放。

清单 5-23 从 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>();
}

如果只需要一个作用域实例,也可以使用泛型OwningComponentBase<T> 基类,它有一个 T 类型的 Service 属性,它将保存 T 类型的作用域实例。清单 5-24 显示了一个示例。 如果您需要创建其他范围实例,您仍然可以使用 ScopedServices 属性。

清单 5-24 使用 OwneringComponentBase<T>

@inherits OwningComponentBase<ScopedService>
<h1>OwningComponent2</h1>
Guid: @Service.Guid

现在将这两个组件添加到 Index 组件中,如清单 5-25 所示。 您可以在 Blazor Server 和 Blazor WebAssembly 之间进行选择。

清单 5-25 使用 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>

运行您的项目并确保您已打开控制台。 现在单击 Counter 组件。 控制台应显示正在处理的 ScopedService 实例。 另请注意,每次实例化 OwningComponentOwningComponent2 时,它们都会收到一个新的 ScopedService 实例。

注意 不要在派生自 OwningComponentBase 的组件上实现 IDisposable ,因为这将停止对作用域实例的自动处置!

实验结果

现在实验完成了,让我们对注入的依赖的生命周期得出一些结论。 每次创建实例时,它都会获得一个新的 GUID。 这样可以很容易地查看是否创建了新实例或重用了相同的实例。

瞬态寿命很容易。 瞬态生命周期意味着您每次都会获得一个新实例。 这对于 Blazor WASM 和 Blazor Server 都是一样的。

单例生命周期意味着在 Blazor WASM 中,您在应用程序的整个持续时间内获得一个实例。 如果您确实需要在所有用户和选项卡之间共享一个实例,则需要将其放在服务器上并通过对服务器的调用来访问它。 但是对于 Blazor Server,每个人都使用相同的实例。 请确保不要将任何用户的信息放在单例中,因为这会泄露给其他用户(糟糕!)。

Blazor WASM 的作用域生命周期与单例生命周期的含义相同。 但是对于 Blazor Server,我们需要小心。 Blazor Server 在浏览器和服务器之间使用 SignalR 连接(称为电路),并且作用域实例链接到电路。 如果您需要特定组件的作用域行为,则可以从 OwningComponentBase 类派生。

对于 Blazor WebAssembly 和 Blazor Server,如果您需要具有相同的实例,则无论用户使用哪个选项卡,您都不能依赖依赖注入来为您执行此操作。 你需要自己做一些状态处理!

建立Pizza服务

让我们回到我们的 PizzaPlace 项目并将其介绍给一些服务。 我可以想到至少两种服务,一种用于检索菜单,另一种用于在用户单击“订购”按钮时下订单。 目前,这些服务将非常简单,但稍后我们将使用它们来建立与服务器的通信。

清单 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");
    }
}

特付 注意State 属性。 我们将从 MenuService 服务(我们将在接下来构建)初始化 State.Menu 属性,并且我们将使用依赖注入来传递服务。

添加 MenuService 和 IMenuService 抽象

如果您使用的是 Visual Studio,请右键单击 PizzaPlace.Shared 项目并选择 Add ➤New Item。 如果您使用的是代码,请右键单击 PizzaPlace.Shared 项目并选择添加文件。 添加一个新的接口类 IMenuService 并完成它,如清单 5-27 所示。

清单 5-27 IMenuService 接口

using System.Threading.Tasks;
namespace PizzaPlace.Shared
{
    public interface IMenuService
    {
        ValueTask<Menu> GetMenu();
    }
}

这个接口允许我们检索一个菜单。 请注意,GetMenu 方法返回一个 ValueTask<Menu>; 那是因为我们希望服务从服务器检索我们的菜单(我们将在接下来的章节中构建它)并且我们希望该方法支持异步调用。

让我们详细说明一下。 首先,更新 Index 组件的 OnInitializedAsync 方法(不要忘记顶部的 @inject),如示例 5-28 所示。 这是一个在声明中使用 async 关键字的异步方法。

永远不要在 Blazor 组件的构造函数中调用异步服务; 始终使用 OnInitializedAsyncOnParametersSetAsync

OnInitializedAsync 方法中,我们使用 await 关键字调用 GetMenu 方法,该关键字要求 GetMenu 返回 Task<Menu>ValueTask<T>。 但是为什么是 ValueTask<T> 而不是 Task? 因为我不知道有人会如何实现 GetMenu 方法。 它们可以同步执行此操作,例如,通过从缓存中检索它,然后使用 Task<T>ValueTask<T> 更昂贵。 此外,ValueTask<T> 是一种值类型,这意味着在同步情况下,该类型不会在堆上结束。

清单 5-28 使用 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");
    }
}

我们还没有准备好运行这个应用程序,因为我们还没有配置依赖注入。 但无论如何都要运行它! 当您收到错误消息时,请查看浏览器的调试器控制台。 您应该看到以下错误:

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'.

依赖注入无法为 IMenuService提供实例。 当然不能! 我们确实实现了这个接口。
将一个新的 HardCodedMenuService 类添加到 PizzaPlace.Shared 项目中,如清单 5-29 所示。 GetMenu 方法返回一个包含三种不同披萨的新 ValueTask<Menu>

清单 5-29 HardCodedMenuService

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)
                }
            });
    }
}

现在我们准备在我们的 Index 组件中使用 IMenuService
从客户端项目中打开 Program.cs。 我们将使用示例 5-30 中所述的瞬态对象。

清单 5-30 为 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();
        }
    }
}

运行 Blazor 项目。 一切都应该仍然有效! 在接下来的章节中,我们将用一个服务来替换它,以从服务器上的数据库中检索所有内容。

通过服务订购比萨饼

当用户选择比萨饼并填写客户信息时,我们希望将订单发送到服务器,以便他们可以预热烤箱并将一些美味的比萨饼发送到客户的地址。 首先向 PizzaPlace 添加一个 IOrderService 接口。 共享项目如清单 5-31 所示。

清单 5-31。 作为 C# 接口的 IOrderService 抽象

using System.Threading.Tasks;
namespace PizzaPlace.Shared
{
    public interface IOrderService
    {
        ValueTask PlaceOrder(ShoppingBasket basket);
    }
}

要下订单,我们只需将购物篮发送到服务器。 在下一章中,我们将构建实际的服务器端代码来下订单; 现在,我们将使用一个伪造的实现,它只是将订单写入浏览器的控制台。 将一个名为 ConsoleOrderService 的类添加到 PizzaPlace.Shared 项目中,如清单 5-32 所示。

清单 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();
        }
    }
}

PlaceOrder 方法只是将购物篮写入控制台。 但是,此方法实现了 .NET 的异步模式,因此我们需要返回一个新的 ValueTask 实例。将 IOrderService 注入到 Index 组件中,如清单 5-33 所示。

清单 5-33 注入 IOrderService

@page "/"
@inject IMenuService MenuService
@inject IOrderService orderService

并通过替换 Index 组件中 PlaceOrder 方法的实现,在用户单击 Order 按钮时使用 order 服务。 由于 orderService 返回一个 ValueTask(与 Task 相同),我们需要使用 await 语法调用它,如清单 5-34 所示。

清单 5-34 异步 PlaceOrder 方法

private async Task PlaceOrder()
{
    await orderService.PlaceOrder(State.Basket);
}

最后一步,配置依赖注入。 同样,我们将 IOrderService 设为瞬态,如清单 5-35 所示。

清单 5-35 为 OrderService 配置依赖注入

builder.Services
.AddTransient<IMenuService, HardCodedMenuService>();
builder.Services
.AddTransient<IOrderService, ConsoleOrderService>();

想想这个。 替换其中一项服务的实施有多难? 只有一个地方说明我们将使用哪个类,即在 Program(或 Startup with Blazor Server)中。 在后面的章节中,我们将构建存储菜单和订单所需的服务器端代码,在之后的章节中,我们将用真正的交易替换这些服务!

再次构建并运行您的项目,打开浏览器的调试器,然后打开控制台选项卡。 订购一些比萨饼,然后单击订购按钮。 您应该会看到一些反馈被写入控制台。

posted @ 2022-09-04 13:54  F(x)_King  阅读(175)  评论(0编辑  收藏  举报