第七章-与微服务通信

使用 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 @   F(x)_King  阅读(137)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战
点击右上角即可分享
微信分享提示
主题色彩