Loading

第七章-与微服务通信

使用 HttpClient 类

首先创建一个新的 Blazor WASM 项目(启用托管),就像您在第一章中创建的一样(称为 Blazor.Communication)。 您将使用此项目来检查为您创建的模板。 您将从查看解决方案的服务器端开始,然后是共享项目的代码,然后是客户端。

检查服务器项目

查看 Blazor.Communication.Server 项目并查找 WeatherForecastController 类,如清单 7-1 所示。

清单 7-1 WeatherForecastController 类

using Blazor.Communication.Shared;
using Microsoft.AspNetCore.Mvc;
namespace Blazor.Communication.Server.Controllers;
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
    private static readonly string[] Summaries = new[]
    {
        "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm",
        "Balmy", "Hot", "Sweltering", "Scorching"
    };
    private readonly ILogger<WeatherForecastController> _logger;
    public WeatherForecastController(
        ILogger<WeatherForecastController> logger)
    {
        _logger = logger;
    }
    [HttpGet]
    public IEnumerable<WeatherForecast> Get()
    {
        return Enumerable.Range(1, 5)
            .Select(index => new WeatherForecast
                    {
                        Date = DateTime.Now.AddDays(index),
                        TemperatureC = Random.Shared.Next(-20, 55),
                        Summary = Summaries[Random.Shared.Next(Summaries.Length)]
                    })
            .ToArray();
    }
}

这看起来是不是有些眼熟? 当然,确实如此; 这是我们在上一章看到的 API 控制器。 您应该使用哪个 URL 来访问 WeatherForecasts 列表?

WeatherForecastController 类在 URI /WeatherForecast 处公开一个 REST 端点,以检索 WeatherForecast 对象列表。 这一次,WeatherForecastController 使用 [Route("[controller]")] 属性来设置端点以一般侦听包含控制器名称(不带后缀“Controller”)的 URI,然后使用 [HttpGet ] 属性以期望 GET 方法。

要调用此方法,您应该在 /weatherforecast URI 上使用 GET,您可以使用浏览器(或者如果您愿意,可以使用 Postman)来尝试。 运行解决方案并在浏览器中输入 URI(不要忘记您可能有不同的端口号),这将导致图 7-1(预计不同的天气;它是随机的)。

image

清单 7-1 中的 Get 方法使用温度和摘要的随机选择来生成这些预测,这非常适合演示。

使用共享项目 为什么?

现在打开 Blazor.Communication.Shared 项目中的 WeatherForecast 类,如清单 7-2 所示。

清单 7-2 共享 WeatherForecast 类

namespace Blazor.Communication.Shared;
public class WeatherForecast
{
    public DateTime Date { get; set; }
    public int TemperatureC { get; set; }
    public string? Summary { get; set; }
    public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}

这个 WeatherForecast 类很简单,包含预测日期、摄氏和华氏温度以及摘要,但我想提请您注意这个类存在于 Shared 项目中的事实。 服务器和客户端项目都使用此共享项目。

如果您曾经使用 JavaScript 创建过 Web 应用程序,那么您应该熟悉为服务器项目构建数据交换类(例如,使用 C#)以及使用 JavaScript(或 TypeScript)为客户端构建另一个类的经验。 您必须确保两个类都序列化为相同的 JSON 格式; 否则,您将收到运行时错误,或者更糟糕的是,丢失数据! 如果模型增长,您必须再次更新这两个类。 在这类项目中,这是一个巨大的维护问题,因为您冒着在繁忙的工作日只更新一侧的风险。

使用 Blazor,您不会受到这种影响,因为服务器和客户端都使用 C#。 这就是为什么有一个共享项目。 您将类放在这里,它们在服务器和客户端之间共享,然后您只需添加对 Shared 项目的引用即可使用它们。 添加另一条数据意味着更新共享类,这很容易! 您不再需要更新两段代码。

查看客户项目

现在查看 Blazor.Communication.Client 项目。 在 Pages 文件夹中,您将找到清单 7-3 中的 FetchData 组件。

清单 7-3 FetchData 组件

@page "/fetchdata"
@using Blazor.Communication.Shared
@inject HttpClient Http
<h1>Weather forecast</h1>
<p>This component demonstrates fetching data from the server.</p>
@if (forecasts == null)
{
	<p><em>Loading...</em></p>
}
else
{
    <table class="table">
        <thead>
            <tr>
                <th>Date</th>
                <th>Temp. (C)</th>
                <th>Temp. (F)</th>
                <th>Summary</th>
            </tr>
        </thead>
        <tbody>
            @foreach (var forecast in forecasts)
            {
                <tr>
                    <td>@forecast.Date.ToShortDateString()</td>
                    <td>@forecast.TemperatureC</td>
                    <td>@forecast.TemperatureF</td>
                    <td>@forecast.Summary</td>
                </tr>
            }
        </tbody>
    </table>
}
@code {
    private WeatherForecast[]? forecasts;
    protected override async Task OnInitializedAsync()
    {
        forecasts = await Http
            .GetFromJsonAsync<WeatherForecast[]>("WeatherForecast");
    }
}

让我们逐行看一下。 第一行添加了路由的路径。 您将在后面的章节中了解路由。 目前,您应该知道,当 URI 为 /fetchdata 时,FetchData 组件将显示在浏览器中。

清单 7-3 中的第二行将共享项目的命名空间的 Razor @using 语句添加到组件。 您需要这个,因为您使用 Shared 项目中的 WeatherForecast 类。 就像在 C# 中一样,您在 Razor 中使用 using 语句来引用来自另一个命名空间的类。

在第三行,您使用 Razor 中的 @inject 语法注入 HttpClient 实例。 HttpClient 类是您用来与服务器通信的类。 您将在本章后面更详细地了解 HttpClient 类。

我确实想指出,您永远不应该自己实例化 HttpClient 类的实例。 Blazor 以一种特殊的方式设置 HttpClient 类,如果您自己创建一个实例,它根本无法按预期工作! 不自己创建实例的另一个原因是这是 FetchData 组件的依赖项,我们在第 5 章中了解到类和组件不应该自己创建依赖项!

在示例 7-3 的下方,您会发现一个 @if 语句。 因为您使用异步方式从服务器获取数据,所以预测字段最初将持有一个空引用。 因此,如果未设置预测字段,则告诉用户等待。如果网络速度较慢,则可以看到这种情况发生。 当您在自己的机器上测试 Blazor 应用程序时,网络很快,但您可以使用浏览器(在本例中使用 Google Chrome)模拟慢速网络。

在 Chrome 中模拟慢速网络

启动 Blazor 项目,以便浏览器打开索引页面。 现在从浏览器打开调试器工具(在带有 Chrome 的 Windows 上,您可以通过按 F12 来执行此操作)并选择 Network 选项卡,如图 7-2 所示。 在右侧,您应该会看到一个下拉列表,允许您选择要模拟的网络类型。 选择慢速 3G。

image

接下来,选择 Blazor 站点上的 Fetch data 选项卡(如果您已经在此选项卡上,请选择另一个选项卡,然后选择 Fetch data 选项卡)。 因为您现在使用的是慢速网络,所以会出现 Loading... 反馈,如图 7-3 所示。

在使用慢速网络测试您的 Blazor 网站后,不要忘记从下图的下拉菜单中选择 No throttling 以将您的网络恢复到正常速度。

image

当 OnInitializedAsync 方法完成时,预测字段保存数据,您的 razor 文件将通过迭代显示一个包含预测的表格,如清单 7-3 的 else 部分所示。

在 FetchData razor 文件的 @code 部分。 首先,您声明一个名为 forecasts 的字段来保存 WeatherForecast 实例的数组。 然后覆盖 OnInitializedAsync 生命周期方法。 因为您使用异步 API 从服务器获取数据,所以您需要将代码放在 OnInitializedAsync 中。 OnInitializedAsync 方法以 C# 的 async 关键字为前缀,这使得使用 await 关键字调用异步 API 变得轻而易举。

异步通信意味着客户端可能需要等待相当长的时间(对于计算机)才能返回结果。 异步调用可能需要很长时间,我们不想阻塞应用程序,所以我们使用异步调用。 不是使用将阻止 Blazor 完成其他请求(冻结用户界面)的调用,而是使用 OnInitializedAsync 方法,该方法将在后台等待结果。

您使用 Http.GetFromJsonAsync<WeatherForecast[]>("WeatherForecast") 在 URI 处调用服务器的 GET 端点,并告诉 GetFromJsonAsync 方法(使用泛型)需要一个 WeatherForecast 对象数组。 当结果从服务器返回时,您将结果放入预测字段,Blazor 将使用您的新数据重新渲染 UI,如图 7-4 所示。

image

了解 HttpClient 类

客户端和服务器之间的所有通信都通过 HttpClient 类。 这是 .NET 中其他应用程序使用的同一个类,它的作用是向服务器发出 HTTP 请求并从服务器公开结果。 它还允许您交换二进制或其他格式的数据,但在 Blazor 中,我们通常使用 JSON。 使用 Blazor WASM,HttpClient 类使用浏览器的网络堆栈在网络上进行通信。

HttpClientJsonExtensions 方法

为了让与 JSON 微服务的对话变得更加容易,.NET 为您提供了一系列方便的扩展方法来处理 .NET 对象和 JSON 之间的转换,您可以在 HttpClientJsonExtensions 类中找到这些方法。 此类位于 System.Net.Http.Json 命名空间中。 我建议你使用这些方法,这样你就不必担心序列化和反序列化 JSON。

GetFromJsonAsync 扩展方法向指定的 URI 发出异步 GET 请求。 它的签名在示例 7-4 中。 也有几个重载可用。

清单 7-4 GetFromJsonAsync 扩展方法签名

public static Task<TValue?> GetFromJsonAsync<TValue>(this HttpClient
client, string? requestUri, JsonSerializerOptions? options,
CancellationToken cancellationToken = default);

因为它是一个扩展方法,所以您可以将其作为 HttpClient 类的普通实例方法来调用,如清单 7-5 所示。
对于其他扩展方法也是如此。

清单 7-5 使用 GetJsonAsync 扩展方法

forecasts = await Http.GetFromJsonAsync<WeatherForecast[]>("WeatherForecast");

GetFromJsonAsync 将期望响应包含由通用参数指定的 JSON。 例如,在示例 7-5 中,它需要一个 WeatherForecast 实例数组。 您通常通过在其前面加上 await 关键字来调用 GetFromJsonAsync 方法。 不要忘记,您只能在异步的方法和 lambda 函数中使用 await 关键字。

您始终可以使用浏览器的调试器检查请求和响应。 运行 Blazor 项目并在“网络”选项卡上打开浏览器的调试器。 现在选择 Blazor 网站中的 Fetch data 选项卡,使其加载数据并查看浏览器的 Network 选项卡,如图所示。

image

在使用清除按钮发出请求之前,您始终可以从以前的请求中清除“网络”选项卡,在 Chrome 中,该按钮看起来像一个带有斜线的圆圈(禁止标志)。

看到图 7-5 中的 WeatherForecasts 条目了吗? 现在您可以单击该条目来查看请求和响应。 让我们从图 7-6 所示的请求预览开始。 使用预览选项卡,您可以看到服务器的响应。

image

如果要查看请求和响应标头,可以单击 Headers 选项卡,如图 7-7 所示。

image

在这里,您可以看到请求的 URL 和 GET 动词(请求方法)。 它还显示 HTTP 状态代码 200 OK。 向下滚动以查看标题。 响应标头之一是 Content-Type,其值为 application/json,由服务器设置,告诉客户端期待 JSON。

PostAsJsonAsync 扩展方法使用在请求正文中序列化的内容参数作为 JSON 发送到指定 URI 的 POST 请求。 它的签名在示例 7-6 中。

清单 7-6 PostAsJsonAsync 方法的签名

public static Task<HttpResponseMessage> PostAsJsonAsync<TValue>(
    this HttpClient client, 
    string? requestUri,
    TValue value, JsonSerializerOptions? options = null,
    CancellationToken cancellationToken = default);

PutAsJsonAsync 扩展方法使用在请求正文中序列化为 JSON 的内容参数向指定 URI 发出 PUT 请求。 它的签名在示例 7-7 中。 它的用法与 PostJsonAsync 非常相似; 唯一的区别是它使用了 PUT 动词。

清单 7-7 PutAsJsonAsync 方法的签名

public static Task<HttpResponseMessage> PutAsJsonAsync<TValue>(
    this HttpClient client,
    string? requestUri, 
    TValue value, 
    JsonSerializerOptions? options = null,
    CancellationToken cancellationToken = default);

使用 JsonSerializerOptions 自定义序列化

这些方法中的每一个都采用一个可选的 JsonSerializerOptions,它允许您控制 JSON 序列化的工作方式。 例如,默认选项将使用属性名称的大小写来序列化属性名称。 但是,有些服务需要骆驼外壳作为属性。 让我们看看如何使用示例 7-8 控制它。 要更改大小写,您可以设置 PropertyNamingPolicy 属性。 在这里,我们将其设置为 JsonNamingPolicy.CamelCase。 此示例还显示了如何控制枚举的序列化。 通常,枚举使用它们的 int 值进行序列化。 例如,Spicy.Spicy 将被序列化为 1。但如果您愿意,也可以使用枚举值的名称,这样 Spiciness.Spicy 将被序列化为“Spicy”。 使用清单 7-8 中的 JsonStringEnumConverter 来做到这一点。 不要忘记,您必须使用 GetFromJsonAsync 和类似方法将 JsonSerializerOptions 作为额外参数传递。

清单 7-8 使用 JsonSerializerOptions 控制大小写

protected readonly JsonSerializerOptions options =
    new JsonSerializerOptions
{
    PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
    Converters =
    {
        new JsonStringEnumConverter()
    }
};

从服务器检索数据

所以现在您已经准备好实现您之前介绍的客户端 IMenuService。打开 PizzaPlace 解决方案并在 PizzaPlace.Client 项目中查看 Program.cs,如清单 7-9 所示。

清单 7-9 Blazor 项目的Program类

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>();
            builder.Services
                .AddTransient<IOrderService, ConsoleOrderService>();
            await builder.Build().RunAsync();
        }
    }
}

我想指出 Main 方法的第三行。 之前,我告诉过你,你永远不应该自己创建 HttpClient 实例。 但我们在这里! 此方法使配置您的 HttpClient 实例变得非常容易,因此服务不必设置 BaseAddress 属性。 这也适用于其他HttpClient 属性。

在 Main 方法中,您添加了两个服务,HardCodedMenuService 和 ConsoleOrderService。 让我们用与服务器对话的真实服务替换这些虚假的实现。

实现菜单服务

在 Visual Studio 中,右键单击 PizzaPlace.Client 项目并从下拉菜单中选择 Add > New Folder。 使用 Code,右键单击 PizzaPlace.Client 项目并选择 New Folder。 将此文件夹命名为服务。 现在向这个文件夹添加一个名为 MenuService 的新类,如清单 7-10 所示。

同样,您正在应用单一职责原则,您可以在服务中封装您与服务器的通信方式。 这样,如果需要,您可以轻松地用另一个实现替换此实现。

清单 7-10 MenuService 类

using PizzaPlace.Shared;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Json;
using System.Threading.Tasks;
namespace PizzaPlace.Client.Services
{
    public class MenuService : IMenuService
    {
        private readonly HttpClient httpClient;
        public MenuService(HttpClient httpClient)
        {
            this.httpClient = httpClient;
        }
        public async ValueTask<Menu> GetMenu()
        {
            var pizzas = await httpClient
                .GetFromJsonAsync<Pizza[]>("/pizzas");
            return new Menu { Pizzas = pizzas!.ToList() };
        }
    }
}

首先向此类添加一个构造函数,获取 MenuService 对 HttpClient 的依赖关系,并将其存储在名为 httpClient 的字段中。 然后实现 IMenuService 接口的 GetMenu 方法,在该方法中与调用服务器 /pizza 端点上的 GetFromJsonAsync 的服务器对话。 请注意,/pizza 端点是相对于站点的基础 (<base href="/" />),可以在 index.html 文件中找到。 您可以在 Program.cs 中更改此基地址(参见清单 7-11)。 因为 MenuService 服务返回的是菜单而不是比萨饼列表,所以您将从服务器获得的比萨饼列表包装到 Menu 对象中。 而已!

你有服务; 现在你需要告诉依赖注入使用MenuService。 在 Program 类的 Main 方法中,替换它,如清单 7-11 所示。

清单 7-11 用 MenuService 替换 HardCodedMenuService

using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using Microsoft.Extensions.DependencyInjection;
using PizzaPlace.Client.Services;
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, MenuService>();
            builder.Services
                .AddTransient<IOrderService, ConsoleOrderService>();
            await builder.Build().RunAsync();
        }
    }
}

运行你的项目。 您应该会看到比萨饼列表(从您的数据库中检索),如图所示! 要在两种实现之间切换(一个假的和一个真正的),我们所要做的就是重新配置依赖注入! 无限力量!!!

image

显示加载 UI

您可能首先会看到一个空菜单,尤其是在慢速网络上。 这可能会让一些客户感到困惑,所以让我们添加一些 UI 来告诉客户稍等一下。 更新 Index.razor 使其类似于示例 7-12。 在这里,我们使用@if 来检查菜单是否已加载,只要菜单上没有比萨饼,我们就在 else 部分使用引导微调器。 如果您愿意,您可以将其替换为任何类型的“加载”UI。 例如,您可以使用 https://tobiasahlin.com/spinkit/。

清单 7-12 向索引组件添加加载 UI

@page "/"
@inject IMenuService MenuService
@inject IOrderService orderService
@if (State.Menu.Pizzas.Any())
{
    <!-- 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()
}
else
{
    <div class="mx-auto text-center mb-3 mt-3">
        <div class="spinner-border text-danger" role="status">
            <span class="visually-hidden">Loading...</span>
        </div>
    </div>
}
@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 async Task PlaceOrder()
    {
        await orderService.PlaceOrder(State.Basket);
    }
}

如果菜单尚未加载,它将显示一个如图 7-9 所示的微调器。

image

存储更改

现在存储来自客户的订单。 因为您还没有用于存储订单的微服务,所以您将首先构建它,然后您将实现客户端服务以将订单发送到服务器。

使用订单更新数据库

什么是订单? 每个订单都有一个客户,并且一个订单有一个或多个披萨。一个披萨可以属于多个订单,这可能会导致一个特定的问题:您需要在披萨和订单之间建立多对多关系,如图所示 图 7-10. 但是使用 Entity Framework Core,这很容易; 再次,这是为您处理的。

image

向 PizzaPlace.Shared 项目添加一个名为 Order 的新类,如清单 7-13 所示。正如预期的那样,我们有一个属性来存储 Customer 和一个 Pizza 实例的集合。还有一个 Id 属性,因为数据库总是需要 一个标识字段,称为主键。

清单 7-13 PizzaOrder 类

using System.Collections.Generic;
namespace PizzaPlace.Shared
{
    public class Order
    {
        public int Id { get; set; }
        public Customer Customer { get; set; } = default!;
        public ICollection<Pizza> Pizzas { get; set; } = default!;
    }
}

接下来,更新 Pizza 类,如清单 7-14 所示。 在这里,我们添加了一个集合来保存订单。 但是,我们不需要从服务器检索订单,这就是我们添加 [JsonIgnore] 属性的原因。 这告诉 JSON 序列化在转换为 JSON 时忽略它。

清单 7-14 Pizza 类

using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace PizzaPlace.Shared
{
    public class Pizza
    {
        public Pizza(int id, string name, decimal price,
                     Spiciness spiciness)
        {
            this.Id = id;
            this.Name = name;
            this.Price = price;
            this.Spiciness = spiciness;
        }
        public int Id { get; }
        public string Name { get; }
        public decimal Price { get; }
        public Spiciness Spiciness { get; }
        [JsonIgnore]
        public ICollection<Order>? Orders { get; set; }
    }
}

现在您可以将这些表添加到 Pizza Place DbContext 类中,如清单 7-15 所示。

清单 7-15 更新的 PizzaPlaceDbContext 类

using Microsoft.EntityFrameworkCore;
using PizzaPlace.Shared;
namespace PizzaPlace.Server
{
    public class PizzaPlaceDbContext : DbContext
    {
        public PizzaPlaceDbContext(
            DbContextOptions<PizzaPlaceDbContext> options)
            : base(options) { }
        public DbSet<Pizza> Pizzas { get; set; } = default!;
        public DbSet<Order> Orders { get; set; } = default!;
        public DbSet<Customer> Customers { get; set; } = default!;
        protected override void OnModelCreating(
            ModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);
            var pizzaEntity = modelBuilder.Entity<Pizza>();
            pizzaEntity.HasKey(pizza => pizza.Id);
            pizzaEntity.Property(pizza => pizza.Name)
                .HasMaxLength(80);
            pizzaEntity.Property(pizza => pizza.Price)
                .HasColumnType("money");
            pizzaEntity.Property(pizza => pizza.Spiciness)
                .HasConversion<string>();
            var ordersEntity = modelBuilder.Entity<Order>();
            ordersEntity.HasKey(order => order.Id);
            ordersEntity.HasOne(order => order.Customer);
            ordersEntity.HasMany(order => order.Pizzas)
                .WithMany(pizza => pizza.Orders);
            var customerEntity = modelBuilder.Entity<Customer>();
            customerEntity.HasKey(customer => customer.Id);
            customerEntity.Property(customer => customer.Name)
                .HasMaxLength(100);
            customerEntity.Property(customer => customer.Street)
                .HasMaxLength(50);
            customerEntity.Property(customer => customer.City)
                .HasMaxLength(50);
        }
    }
}

在这里,您添加了 Customers 和 Orders 表,并且在 OnModelCreating 方法中,您向 Entity Framework Core 解释了应该如何映射事物。

客户有一个主键 ID 及其长度限制的字符串属性。 一个Order有一个主键Id和一个Customer,和Pizza是多对一的关系(一个Order可以有很多Pizza,每个Pizza可以属于很多Order)。

构建您的项目并修复您可能遇到的任何编译器错误。

现在是时候创建另一个迁移了。 此迁移将使用您的新表更新您的数据库。 在 Visual Studio 中,打开 Package Manager Console(您可以通过 View > Other Windows > Package Manager Console 找到它)。 使用代码,打开集成终端。 如果您愿意,也可以使用命令行(我真的很喜欢 Windows 10 中的新终端)。 将目录更改为 PizzaPlace.Server 项目。

现在输入以下命令:

dotnet-ef migrations add Orders

这将为您的新数据库架构创建迁移。
通过键入以下命令将迁移应用到您的数据库:

dotnet-ef database update

数据库部分到此结束。

构建订单微服务

是时候构建接受订单的微服务了。 在 Visual Studio 中,右键单击 PizzaPlace.Server 项目的 Controllers 文件夹并选择 New > Controller。 选择一个 Empty API Controller 并将其命名为 OrdersController。 使用 Code,右键单击 PizzaPlace.Shared 项目的 Controllers 文件夹并选择 New File,将其命名为 OrdersController。 这个类可以在清单 7-16 中找到。

清单 7-16 OrdersController 类

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using PizzaPlace.Shared;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace PizzaPlace.Server.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class OrdersController : ControllerBase
    {
        private readonly PizzaPlaceDbContext db;
        public OrdersController(PizzaPlaceDbContext db)
        {
            this.db = db;
        }
        [HttpPost("/orders")]
        public IActionResult InsertOrder([FromBody] ShoppingBasket basket)
        {
            Order order = new Order();
            order.Customer = basket.Customer;
            order.Pizzas = new List<Pizza>();
            foreach (int pizzaId in basket.Orders)
            {
                var pizza = db.Pizzas.Single(p => p.Id == pizzaId);
                order.Pizzas.Add(pizza);
            }
            db.Orders.Add(order);
            db.SaveChanges();
            return Created("/orders", order.Id);
        }
    }
}

OrdersController 需要一个 PizzaPlaceDbContext,因此您添加一个构造函数来获取该实例,然后让依赖注入来处理其余的事情。 要创建新订单,请使用 InsertOrder 方法的 POST 动词,在请求正文中获取 ShoppingBasket 实例。

收到购物篮实例后,您创建订单,然后设置订单的客户。 接下来,您用比萨饼填充订单的 Pizzas 集合。 我们收到了 Pizzas 的 ID,所以我们用它来查找它们。 然后我们将新的订单实例添加到 PizzaPlaceDbContext Orders 集合中。 现在当我们调用 SaveChanges 时,Entity Framework 会将它插入到 Orders 表中。 而已。 Entity Framework Core 完成了存储数据的所有工作!

与订单微服务交谈

将一个名为 OrderService 的新类添加到 PizzaPlace.Client 项目的 Services 文件夹中。 这个 OrderService 使用对服务器的 POST 请求,如清单 7-17 所示。

清单 7-17 OrderService 类

using PizzaPlace.Shared;
using System.Net.Http;
using System.Net.Http.Json;
using System.Threading.Tasks;
namespace PizzaPlace.Client.Services
{
    public class OrderService : IOrderService
    {
        private readonly HttpClient httpClient;
        public OrderService(HttpClient httpClient)
        {
            this.httpClient = httpClient;
        }
        public async ValueTask PlaceOrder(ShoppingBasket basket)
        {
            await httpClient.PostAsJsonAsync("/orders", basket);
        }
    }
}

首先,向 OrderService 类添加一个构造函数,获取存储在 OrderService 类的 httpClient 字段中的 HttpClient 依赖项。 接下来,通过添加 PlaceOrder 方法来实现 IOrderService 接口,并将 ShoppingBasket 作为参数。 最后,您使用 await 关键字调用异步 PostAsJsonAsync 方法。

现在从 PizzaPlace.Client 项目中打开 Program 类,并将 ConsoleOrderService 类替换为新的 OrderService 类,如清单 7-18 所示。

清单 7-18 配置依赖注入以使用 OrderService 类

using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using Microsoft.Extensions.DependencyInjection;
using PizzaPlace.Client.Services;
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,
            MenuService>();
            builder.Services.AddTransient<IOrderService,
            OrderService>();
            await builder.Build().RunAsync();
        }
    }
}

运行您的 PizzaPlace 应用程序并订购几个比萨饼。 现在在 Visual Studio(或 SQL Operations Studio)中打开 SQL Server 对象资源管理器并检查客户和订单表。 它们应该包含您的新订单。 您还将在数据库中看到另一个表,即 OrderPizza 表。 该表由实体框架生成,用于存储 Orders 和 Pizzas 之间的多对多关系.

posted @ 2022-09-04 14:57  F(x)_King  阅读(116)  评论(0编辑  收藏  举报