【译】Visual Studio 2022 中的 Web API 开发

  在 Visual Studio 2022 中,Web 开发人员的主要场景之一是使用 ASP.NET Core 创建 Web API。在 Visual Studio 2022 17.6 的最新预览版中,我们添加了一些更新,以便在开发 API 时更高效。在这篇文章中,我们将介绍一个从头开始的新的 API 开发的示例场景,并在此过程中指出新特性。

  我们将在这篇文章中介绍的一些新特性包括:

  * 带有集成客户端的 HTTP 编辑器

  * API Endpoint 资源管理器

  * 脚手架

  * Entity Framework 工具

  在这篇文章中,我们将展示从一个新项目开始开发一个完整的 Web API 的完整的端到端。下面是这篇文章的概要。

  * 创建一个新的 API 项目

  * 向项目中添加模型

  * 使用脚手架生成 API

  * 使用 API Endpoint 资源管理器

  * 使用 HTTP 编辑器

开始

  要在 Visual Studio 中开发 Web API,第一步是创建一个新项目。在 Visual Studio 2022 中,您可以使用“新建项目”对话框创建一个新项目。在这篇文章中,我们将要为一个虚构的外卖餐厅创建一个 ASP.NET Core Web API。按照教程,创建一个名为 MyRestaurantService 的项目,并在附加信息页面中选择以下选项。

  在这个示例中,我们使用的是 Endpoint,而不是基于 Controller 的 API。API Endpoint 和基于 Controller 的 API 的步骤是相同的,只是适用场合不同。

  既然已经创建了项目,我们要做的第一件事就是为我们想要通过 API 公开的对象添加一些模型类。我们将需要向项目添加以下类/枚举。在下表中,您将看到要创建的对象列表。

名称

描述

Contact

获取正在下订单的客户

MenuItem

表示餐厅菜单中的菜单项

MenuItemCategory

表示菜单项类别的枚举

MenuItemOrdered

表示已添加到客户订单中的项目

OrderStatus

表示已提交订单的状态

PaymentMethod

表示用于订单的付款方式的枚举

TogoOrder

表示已下单的订单

  添加的文件要么是标准 POCO 类,要么是枚举。例如,Contact.cs:

namespace MyRestaurantApi; 
public class Contact {
    public int Id { get; set; }
    public string? Name { get; set; }
    public string? Email { get; set; }
    public string? Phone { get; set; }
}

  这些对象表示一组数据,可用于向最终用户显示菜单以及提交 Togo 订单。现在我们已经添加了这些模型类,下一步是在 Visual Studio 中使用“脚手架”来创建 API Endpoint 并连接 Entity Framework DbContext。我们需要创建4组 API Endpoint。

  * Contact

  * MenuItem

  * MenuItemsOrdered

  * TogoOrder

  对于每个模型类,我们将使用脚手架来处理生成初始 API Endpoint 的艰苦工作。让我们开始吧。

Visual Studio 中的脚手架

  要开始使用脚手架,请右键单击 ASP.NET Core Web 项目 MyRestaurantApi,然后选择 Add > New Scaffolded Item。请参见下图。

  调用此菜单项后,将显示脚手架对话框。在这个对话框中,我们将选择要运行的脚手架类型然后配置它。对于这个项目,我们将使用 API with read/write endpoints, using Entity Framework 脚手架。

  选择此选项后,您将看到一个新对话框,提示您配置脚手架选项。下表总结了主要备选方案。

选项

描述

Model class

脚手架将生成读取/写入所选模型类值的 API

Endpoint class

应该向其中写入新终结点的类。如果选择现有类,则终结点将添加到该类。您还可以使用 + 按钮创建一个新类

DbContext class

Entity Framework DbContext,它将管理对数据库的读/写操作。您可以使用 + 按钮创建一个新的 DbContext

Database Provider

要使用的数据库提供程序,这将由要存储此数据的数据库决定

  我们将从 Contact 模型开始。在这种情况下,我们将指定 Contact 类作为模型类,创建一个新的终结点类以及一个新 DbContext。我们将在此示例中使用 Sqlite,但您可以根据需要选择列出的任何提供商。下面是配置了这些选项的对话框。

  使用+按钮创建新的终节点类和 DbContext 类。可以使用在为这两个选项单击+按钮时提供的默认值。下一步是单击 Add 按钮。单击 Add 后,将使用 NuGet 安装脚手架工具,然后调用它来生成新文件。下面列出了添加或修改的文件。

  Project 文件——修改项目文件以添加支持更改所需的包引用以及脚手架工具本身的包引用。

  Program.cs——修改 Program.cs 文件以添加 EF DbContext 以及注册新的终结点类。

  appSettings. json——添加连接字符串。

  ContactEndpoints.cs——这是一个新添加的类,用于处理 API 请求/响应。

  下面是为 ContactEndpoints 类生成的代码。

using Microsoft.EntityFrameworkCore;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.OpenApi;
using MyRestaurantApi.Data;
namespace MyRestaurantApi;

public static class ContactEndpoints
{
    public static void MapContactEndpoints (this IEndpointRouteBuilder routes)
    {
        var group = routes.MapGroup("/api/Contact").WithTags(nameof(Contact));

        group.MapGet("/", async (MyRestaurantApiContext db) =>
        {
            return await db.Contact.ToListAsync();
        })
        .WithName("GetAllContacts")
        .WithOpenApi();

        group.MapGet("/{id}", async Task<Results<Ok<Contact>, NotFound>> (int id, MyRestaurantApiContext db) =>
        {
            return await db.Contact.AsNoTracking()
                .FirstOrDefaultAsync(model => model.Id == id)
                is Contact model
                    ? TypedResults.Ok(model)
                    : TypedResults.NotFound();
        })
        .WithName("GetContactById")
        .WithOpenApi();

        group.MapPut("/{id}", async Task<Results<Ok, NotFound>> (int id, Contact contact, MyRestaurantApiContext db) =>
        {
            var affected = await db.Contact
                .Where(model => model.Id == id)
                .ExecuteUpdateAsync(setters => setters
                  .SetProperty(m => m.Id, contact.Id)
                  .SetProperty(m => m.Name, contact.Name)
                  .SetProperty(m => m.Email, contact.Email)
                  .SetProperty(m => m.Phone, contact.Phone)
                );

            return affected == 1 ? TypedResults.Ok() : TypedResults.NotFound();
        })
        .WithName("UpdateContact")
        .WithOpenApi();

        group.MapPost("/", async (Contact contact, MyRestaurantApiContext db) =>
        {
            db.Contact.Add(contact);
            await db.SaveChangesAsync();
            return TypedResults.Created($"/api/Contact/{contact.Id}",contact);
        })
        .WithName("CreateContact")
        .WithOpenApi();

        group.MapDelete("/{id}", async Task<Results<Ok, NotFound>> (int id, MyRestaurantApiContext db) =>
        {
            var affected = await db.Contact
                .Where(model => model.Id == id)
                .ExecuteDeleteAsync();

            return affected == 1 ? TypedResults.Ok() : TypedResults.NotFound();
        })
        .WithName("DeleteContact")
        .WithOpenApi();
    }
}

  这里使用 ASP. NET Core 最小化 API,如前所述,如果你更喜欢基于 Controller 的 API,一个新的 Controller 将被添加到 Controllers 文件夹中。

  现在我们已经为 Contact 类生成了 API 终结点,我们需要为剩下的三个模型类做同样的事情。我们将重复相同的步骤,但有一点不同。我们不选择创建新的 DbContext,而是选择在此步骤中创建的 DbContext,以便为其他三个步骤重用。由于要选择现有的 DbContext,因此数据库提供程序选项将被禁用,因为它已经在 DbContext 中配置了。现在你可以为这些类搭建 API 终结点:MenuItem, MenuItemsOrdered 和TogoOrder。

  完成这三个文件的搭建后,您的解决方案资源管理器应该与下图类似。

  在这个时间点上,建议构建下解决方案以确保生成的代码没有问题。在这一步中,我们使用 Visual Studio 脚手架创建了一组连接到实体框架数据库的 API 终结点。下一步是进行数据库设置。

  现在我们有了 DbContext,我们想要向数据库中添加一些初始数据,这通常被称为数据播种。在我们的示例中,我们需要填 MenuItems 以及 Contact 表。我们可以通过定制搭建过程中生成的 DbContext 类来实现这一点。我们将添加两个方法来返回数据,并且我们将重写  OnModelCreating 方法来向数据库注册数据。我们将添加的返回初始数据的方法是 GetSeedDataMenuItems 和 GetSeedDataContacts。这些方法分别返回 MenuItems 或 Contacts 的数组。然后我们在 OnModelCreating 方法中调用那些方法。代码现在看起来应该如下所示。

public class MyRestaurantApiContext : DbContext {
    public MyRestaurantApiContext(DbContextOptions<MyRestaurantApiContext> options)
        : base(options) {
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder) {
        modelBuilder.Entity<MenuItem>().HasData(GetSeedDataMenuItems());
        modelBuilder.Entity<Contact>().HasData(GetSeedDataContacts());
    }

    public DbSet<MyRestaurantApi.Contact> Contact { get; set; } = default!;

    public DbSet<MyRestaurantApi.MenuItem> MenuItem { get; set; } = default!;

    public DbSet<MyRestaurantApi.MenuItemOrdered> MenuItemOrdered { get; set; } = default!;

    public DbSet<MyRestaurantApi.TogoOrder> TogoOrder { get; set; } = default!;
    private MenuItem[] GetSeedDataMenuItems() => new MenuItem[] {
        new MenuItem {
            Id = 1,
            Name = "Hamburger",
            Price = (decimal)3.68,
            Description = "It's a cheese burger without the cheese",
            Category = MenuItemCategory.Lunch
        },
        new MenuItem {
            Id = 2,
            Name = "Hamburger - double",
            Price = (decimal)5.70,
            Description = "It's a cheese burger without the cheese, with two beef patties",
            Category = MenuItemCategory.Lunch
        },
        new MenuItem {
            Id = 3,
            Name = "Cheeseburger",
            Price = (decimal)4.09,
            Description = "A hamburger with cheese",
            Category = MenuItemCategory.Lunch
        },
        new MenuItem {
            Id = 4,
            Name = "Cheeseburger - double",
            Price = (decimal)5.09,
            Description = "A hamburger with cheese, with two beef patties",
            Category = MenuItemCategory.Lunch
        },
        new MenuItem {
            Id = 5,
            Name = "Mushroom & Swiss burger",
            Price = (decimal)4.59,
            Description = "Mushroom & Swiss burger",
            Category = MenuItemCategory.Lunch
        },
        new MenuItem {
            Id = 6,
            Name = "Mushroom & Swiss burger - double",
            Price = (decimal)6.09,
            Description = "Mushroom & Swiss burger, with two beef patties",
            Category = MenuItemCategory.Lunch
        }
    };
    private Contact[] GetSeedDataContacts() => new Contact[] {
        new Contact {
            Id = 1,
            Name = "Sayed Hashimi",
            Email = "sayed@example.com",
            Phone = "555-111-2222"
        },
        new Contact {
            Id=2,
            Name = "Mads Kristensen",
            Email = "mads@example.com",
            Phone = "555-111-3333"
        },
        new Contact {
            Id=3,
            Name = "Eline Barstad",
            Email = "elineb@example.com",
            Phone = "555-111-4444"
        },
        new Contact {
            Id=4,
            Name = "Theodore Lamy",
            Email = "theol@example.com",
            Phone = "555-111-5555"
        },
        new Contact {
            Id=5,
            Name = "María Zelaya",
            Email = "mariaz@example.com",
            Phone = "555-111-6666"
        },
        new Contact {
            Id=6,
            Name = "Kubanychbek Sagynbek",
            Email = "kubans@example.com",
            Phone = "555-111-7777"
        },
        new Contact {
            Id=7,
            Name = "Denise Bourgeois",
            Email = "deniseb@example.com",
            Phone = "555-111-8888"
        },
        new Contact {
            Id=8,
            Name = "Robin Danielsen",
            Email = "robind@example.com",
            Phone = "555-111-9999"
        }
    };
}

  现在,我们已经完成了获取 DbContext 和数据模型以支持此应用程序所需的所有工作。现在我们可以继续准备数据库本身,到目前为止,我们只定义了一些代码,但没有将任何代码应用于任何数据库。我们将在 Visual Studio 中使用一些新的实体框架支持来简化这一点。

Visual Studio 中的实体框架支持

  在使用 API 端点之前,我们需要配置数据库。在搭建步骤中,在代码中定义了 EF DbContext,但是数据库仍然没有配置。要做到这一点,我们需要执行两个操作。

  * 添加EF迁移

  * 更新数据库

  在过去,您必须使用 dotnet ef 命令行工具来执行这两个步骤。在新版  Visual Studio 中,我们添加了支持,这样您就不需要使用命令行来进行这些操作了。在 ASP. NET Core 的 Connected Services 选项卡中添加了支持。双击解决方案资源管理器中 Web 项目 MyRestaurantApi 下的  Connected Services 节点。当您进入该页面时,它看起来如下所示。

  由于我们之前配置的是使用 Sqlite,因此我们将使用 Sqlite(Local) 条目右侧的…菜单中的一些可用选项。当您单击该菜单时,您应该看到以下选项。

  首先,我们需要创建一个新的实体框架迁移。我们使用 Add migration  选项。当您单击该选项时,将出现 Entity Frameworks Migration 对话框。在进入此菜单之前,请确保项目未运行。对话框将启动构建,如果项目正在运行,您将收到一个错误。当您进入这个对话框时,它看起来应该类似于下面的图片。

  在这个页面上有两个输入,一个是要创建的迁移的名称,另一个是要使用的 DbContext 类。迁移总是有一个可以使用的默认名称,或者如果您愿意,也可以应用一个特定的名称。这将是添加迁移时生成的文件的名称。对于 DbContext,它应该自动选择在搭建步骤中创建的 DbContext。对于包含多个 DbContext 的项目,您应该选择您所针对的特定 DbContext。现在我们可以单击 Finish 来添加迁移。单击 Finish 后,将创建迁移并将其添加到项目中。下一步是通过运行该迁移来更新数据库。

  要更新数据库,我们将使用 Update database 菜单选项。当您单击该条目时,将出现一个对话框,您可以在其中选择目标 DbContext。在这个示例中,它应该默认为在前面步骤中创建的上下文。如下图所示。

  从这里开始,我们只需要单击 Finish,这将在数据库上运行前一步中的迁移。单击 Finish 后,数据库已经配置完毕,可以使用了。如果您正在使用源代码管理,那么现在是创建新提交的好时机。在对模型类进行任何进一步更改之后,您应该重复这两个步骤来更新数据库。让我们继续看看我们现在如何使用这些搭建起来的api。我们将使用新的终结点资源管理器开始。

Endpoints Explorer

  Endpoints Explorer 是我们正在开发的一个新的预览特性,它使您能够查看解决方案中定义的 API 终结点并与之交互。由于这是一个预览功能,因此需要启用它。要启用这个新的工具窗口,请进入 Tools >  Options >  Environment >  Preview Features,然后勾选 Web API Endpoints Explorer。您可以使用该对话框中的搜索框来筛选。请看下面的图片。

  现在你已经启用了 Endpoints Explorer,你可以通过 View >  Other Windows >  Endpoints Explorer 进入。打开该窗口后,您应该看到类似于下图所示的内容。

  在这个窗口中,您可以看到在 Visual Studio 中使用脚手架生成的所有 API 终结点。它为我们在搭建过程中使用的每个模型类创建了5个终结点(Get/Post/Get specific/Put/Delete)。在此视图中,您可以查看解决方案包含的所有终结点。如果您将 API 终结点添加到项目中,您可能需要使用 Endpoints Explorer 顶部的 Refresh 按钮来刷新视图。对于每个请求,您都可以查看处理请求的代码以及生成到该终结点的新请求的代码。当您单击某个条目时,您可以在上下文菜单中看到这些选项。这将在下一个屏幕截图中显示。

  在这里,如果您调用 Generate Request,HTTP 文件将与请求一起生成。对于 /api/MenuItem/ 请求,这里是调用上下文菜单后的结果。

  点击后,出现以下情况:

  1 创建一个 HTTP 文件并将其添加到项目中(如果在生成请求时打开了 HTTP 文件,则会将其添加到该文件中)。

  2 在 HTTP 文件中添加一个变量,包含 Web 项目的 Web 地址。

  3 将请求添加到文件中。

  Endpoints Explorer 目前是一个预览功能,因此它不会包含在 isual Studio 2022 17.6,它将出现在17.6和17.7的预览版本中。我们希望能尽快将其添加到 GA 构建中。

  HTTP 文件是添加到 Visual Studio 中的一种新文件类型。现在让我们回顾一下对 HTTP 文件的支持。

HTTP 编辑器

  我们在 Visual Studio 2022 中增加了对.HTTP (.REST) 文件的支持。这种支持的灵感来自于伟大的 VS Code Extension REST Client。这个扩展在 VS Code 中很流行,现在其他工具也支持这个文件格式。当我们在 Visual Studio 中添加对 HTTP 文件的支持时,我们最初想直接集成这个扩展,但是这个扩展是通过直接调用 VS Code 可扩展性 api 来实现的,所以这不是一个选择。我们在 Visual Studio 中开发了一个新的 HTTP 编辑器来支持这种文件类型。目前,REST 客户端扩展的语法在 Visual Studio 中还不支持,但我们正在努力弥补这一差距。既然我们已经讨论了这个新编辑器的起源,让我们来探索一下它的功能。

  HTTP 编辑器的主要目的是使您能够声明一个或多个 HTTP 请求,并使您能够查看/检查发送请求时产生的响应。让我们仔细看看为 /api/MenuItem/ 终结点生成的代码。代码如下。

@MyRestaurantApi_HostAddress = https://localhost:7020

Get {{MyRestaurantApi_HostAddress}}/api/MenuItem/

###

  我们看一下这里的每一行。

@MyRestaurantApi_HostAddress = https://localhost:7020

  第一行声明了一个名为 MyRestaurantApi_HostAddress 的变量,并将其值赋给 https://localhost:7020。URL 来自添加 HTTP 文件的项目。声明一个新变量的语法是:

@VariableName = value

  这些变量可以在 HTTP 文件中的任何地方声明。您可以在 HTTP 文件的声明之后使用这些值。让我们看一下下一行,看看如何使用变量。

Get {{MyRestaurantApi_HostAddress}}/api/MenuItem/

  这表示一个 HTTP 请求。在定义 HTTP 请求时,第一个值是 HTTP 请求方法,在本例中是 GET。后面跟着 URL。在本例中,我们使用的是前一行声明的变量。要获取变量的值,请使用 HTTP 文件中的语法{{VariableName}}。在这一行,如果需要指定要使用的特定 HTTP 版本,可以在 URL 后面添加它。例如,对于相同的请求,如果我们想使用 HTTP 1.1,代码行将如下所示。

Get {{MyRestaurantApi_HostAddress}}/api/MenuItem/ HTTP/1.1

  现在让我们转到文件的最后一行。

###

  这表示 HTTP 请求的结束。之后,您可以向该文件添加其他 HTTP 请求。现在让我们看一个示例,该示例还包含请求的正文和一些报头。

  我们查看的示例是一个简单的 GET 请求,请求中没有添加报头或正文。对于 HTTP 请求来说,正文和报头是非常常见的,所以让我们看看它们是什么样子的。在这种情况下,我们可以在 Endpoints Explorer 中生成一个请求,然后添加 body 和 header。

  前面我们看到,我们有一个 MenuItem API 终结点,用于 get/create/edit 可用于订购的菜单项。假设我们想要添加一个新的 MenuItem,为此我们需要创建一个 POST 请求来添加一个新的 MenuItem。我们可以使用 Endpoints Explorer 来帮助生成该请求。在 POST /api/MenuItem/上右键选择 Generate Request。

  在当前版本的 Endpoints Explorer 中,这将为 HTTP 文件生成以下内容。

Post {{MyRestaurantApi_HostAddress}}/api/MenuItem/

###

  在未来的版本中,我们希望为这个请求所需的 JSON 主体提供一个桩。从这里开始,我们要添加 JSON 主体和头,以便演示如何使用它们。我们需要添加到主体的 JSON 应该表示带有相应设置值的 MenuItem 的 JSON,不包括将由数据库设置的 id 字段。更新后的请求如下所示。

Post {{MyRestaurantApi_HostAddress}}/api/MenuItem/
Content-Type: application/json
Accept: application/json

{
  "name": "All-beef hotdog and soda",
  "price": 1.50,
  "description": "An all-beef hotdog and soda with a permanent fixed price.",
  "category": 1
}

###

  让我们更详细地描述这个请求的语法。请求的语法是,第一行是 HTTP 方法,后面是 URL 和可选的 HTTP 版本,如前所述。在请求行之后,您可以在请求行之后的单独行中指定每个标头(中间没有空白行)。当指定一个报头时,语法是 HeaderName: Value,其中 HeaderName 是传入的报头的名称,Value 是您为它指定的值。在本例中,我们指定了两个报头, Content-Type 和 Accept。这个请求不需要这些标头,因为它们是默认的,但是这里展示它们是为了演示如何传入标头。在报头或请求行(如果没有报头)之后,您应该添加一个空行,后面跟着请求正文。最后,我们在末尾添加###来表示请求的结束。

  在这个请求中,我们将添加一个新的 MenuItem,它将用于订购。我们增加了一种新的“全牛肉热狗加苏打水套餐”。首先,我们希望通过从 Visual Studio 启动 API 来确保它正在运行。在启动 Web API 之后,可以使用请求行左侧的绿色播放按钮发送请求。在 Visual Studio 中,结果应该与以下类似。

  在这里,我们可以看到添加“全牛肉热狗和苏打水”的请求已成功添加到数据库中。数据库中为它分配了 id 值为7。现在我们可以创建一个请求来添加一个新的 Togo 订单。在提交新的 TogoOrder 之前,我们需要修改处理 POST 方法的 TogoOrderEndpoints 类。当提交 TogoOrder 时,如果 OrderCreated 值为空,则应该将其设置为当前日期/时间,并且我们还希望从 MenuItem 值中填充 MenuItemOrdered 的详细信息。下面是更新后的 TogoOrder Post 处理程序代码。更新后的代码如下。

group.MapPost("/", async (TogoOrder togoOrder, MyRestaurantApiContext db) =>
{
    togoOrder.Customer = await db.Contact.FindAsync(togoOrder.Customer!.Id);

    if (togoOrder.OrderCreated == null) {
        togoOrder.OrderCreated = DateTime.Now;
    }
    if (togoOrder.ItemsOrdered != null && togoOrder.ItemsOrdered.Count > 0) {
        foreach (var item in togoOrder.ItemsOrdered) {
            var menuItem = await db.MenuItem.FindAsync(item.MenuItemId);
            item.Name = menuItem!.Name;
            if (item.Price is null || !item.Price.HasValue || item.Price.Value < 0) {
                item.Price = menuItem.Price!.Value;
            }
            if (item.Category is null || !item.Category.HasValue) {
                item.Category = menuItem.Category!.Value;
            }
        }
    }
    db.TogoOrder.Add(togoOrder);
    await db.SaveChangesAsync();
    return TypedResults.Created($"/api/TogoOrder/{togoOrder.Id}",togoOrder);
})
.WithName("CreateTogoOrder")
.WithOpenApi();

  需要添加的代码出现在 db.TogoOrder.Add(togoOrder) 之前。这是不言自明的。当提交请求时,如果 OrderCreated 为空,则填充它的值,然后从 MenuItem 中缺失的任何值将从相应的 MenuItemId 中填充。MenuItemId 是将要创建的每个 MenuItemOrdered 的唯一必需值。

  首先,我们将使用 Endpoints Explorer 生成桩请求,方法是右键单击 POST /api/TogoOrder/并选择 generate request。然后我们需要将 JSON 主体添加到请求中。添加 JSON 主体后,请求应该如下所示。

Post {{MyRestaurantApi_HostAddress}}/api/TogoOrder/
Content-Type: application/json

{
  "itemsOrdered": [
    {
      "menuItemId":1
    }
  ],
  "subtotal": 3.50,
  "tax": 0.8,
  "total": 4.30,
  "paymentMethod": 1,
  "customer": {
    "id": 2
  }
}
###

  在这个请求中,我们使用 POST 方法添加一个新的 TogoOrder 条目。如前所述,我们可以列出订购的项目,唯一必需的字段是 menuItemId。我们添加到 Post 方法中的代码将填充缺失的字段。我们还在 TogoOrder 对象中分配了美元金额。我们可以通过按请求行左侧的绿色播放按钮发送此请求。调用之后,响应应该类似于以下内容。

{
  "id": 1,
  "orderCreated": "2023-04-27T14:14:48.3785278-04:00",
  "itemsOrdered": [
    {
      "id": 1,
      "menuItemId": 1,
      "togoOrderId": 1,
      "name": "Hamburger",
      "price": 3.68,
      "category": 1
    }
  ],
  "subtotal": 3.5,
  "tax": 0.8,
  "total": 4.3,
  "paymentMethod": 1,
  "customer": {
    "id": 2,
    "name": "Mads Kristensen",
    "email": "mads@example.com",
    "phone": "555-111-3333"
  }
}

  现在我们已经将 TogoOrder 添加到数据库中,让我们通过发出一个 GET 请求来列出数据库中的所有 TogoOrder,从而验证我们获得了正确的结果。通过在 GET /api/TogoOrder/端点的上下文菜单中选择 generate request,我们可以使用 Endpoints Explorer 生成该请求。调用后,将向 HTTP 文件中添加以下请求。

Get {{MyRestaurantApi_HostAddress}}/api/TogoOrder/

###

  要发送此请求,请单击绿色播放按钮。这样做之后,响应视图中的结果应该类似于以下内容。

[
  {
    "id": 1,
    "orderCreated": "2023-04-27T14:22:53.2117889",
    "itemsOrdered": null,
    "subtotal": 3.5,
    "tax": 0.8,
    "total": 4.3,
    "paymentMethod": 1,
    "customer": null
  }
]

  我们已经成功地收到了结果,但是这里 itemsOrdered 为 null。如果我们要检查数据库,我们将看到所有正确的条目已经创建,所有的关系也都是正确的。这里的问题是实体框架没有自动加载相关数据。有关EF如何加载相关数据的更多信息,请参《 Loading Related Data – EF Core》。在本例中,当返回 TogoOrder 时,我们总是希望加载 ItemOrdered 字段的数据。我们需要对生成的 GET 请求进行简单的编辑以满足这一需求。在本例中,当返回 TogoOrder 时,我们总是希望加载 ItemOrdered 字段的数据。我们需要对生成的 GET 请求进行简单的编辑以满足这一需求。处理此请求的方法在 TogoOrderEndpoints 类中。我们需要为 EF 添加一个 include 语句来加载 ItemOrdered 属性。修改后的方法如下:

group.MapGet("/", async (MyRestaurantApiContext db) =>
{
    return await db.TogoOrder
        .Include(order => order.ItemsOrdered)
        .ToListAsync();
})

  添加此内容后,我们将重新启动 Web API 项目并重新发送 GET 请求以列出所有订单。结果应该类似于下面的代码片段。

[
  {
    "id": 1,
    "orderCreated": "2023-04-27T14:22:53.2117889",
    "itemsOrdered": [
      {
        "id": 1,
        "menuItemId": 1,
        "togoOrderId": 1,
        "name": "Hamburger",
        "price": 3.68,
        "category": 1
      }
    ],
    "subtotal": 3.5,
    "tax": 0.8,
    "total": 4.3,
    "paymentMethod": 1,
    "customer": null
  }
]

  现在我们可以看到,请求返回了 TogoOrder 对象,其中填充了项目的 Ordered 值。我们现在已经成功地构建了一个 ASP. NET Core Web API 来处理订单请求。让我们继续讨论 HTTP 文件中支持的语法。

HTTP 文件语法

  之前我们提到过 HTTP 编辑器及其相关的支持是受到 Visual Studio Code REST Client extension 的启发。该扩展在 HTTP 文件中支持的语法比 Visual Studio 现在支持的更广泛。随着我们的进展,我们将增加更多的支持来缩小差距。在本节中,我们将讨论 Visual Studio 当前支持的语法。

  在本节中,请求将使用 httpbin.org,这是每个人都可以使用的免费第三方 Web API。您可以向它发送请求,它会将请求回显给您。让我们来探索一下 Visual Studio 目前支持的语法。

注释

  注释是以#或//开头的行。这些行将被忽略,并且不会包含在请求中。

变量

  如前所述,您可以定义可在后续请求中重用的变量。您可以定义所需的任意数量的变量。可以使用已经定义的其他变量的值来定义变量。例如,请参见下面的代码片段。

@searchTerm = some-search-term
@hostname = httpbin.org
# variable using another variable
@host = https://{{hostname}}
@name = Sayed
@phone = 111-222-3333

   这里我们定义了几个变量,包括主机变量,它使用另一个名为 hostname 的变量的值。下面是使用这些变量之一的请求示例。

GET {{host}}/anything HTTP/1.1
User-Agent: rest-client
Content-Type: application/json
###

请求行

  对于请求行,我们前面提到了请求行的格式是:

HTTPMethod URL HTTPVersion

  这里的 HTTPMethod 是要使用的 HTTP 方法,例如 GET、POST、PUT、PATCH 等。URL 是将请求发送到的 URL。此 URL 还可以包括查询字符串参数,如以下示例请求。可选的 HTTPVersion 是应该使用的 HTTP 版本。下面是一些示例请求。

GET https://httpbin.org/get
###

GET https://httpbin.org/get?name=Sayed?&phone=111-222-3333
###

GET https://httpbin.org/get?name=Sayed?&phone=111-222-3333 HTTP/1.1
###

  Visual Studio Code REST Client extension 支持一些我们现在不支持的功能。这些包括。

  * 可选HTTP方法 REST Client 支持不指定 HTTP 方法。在这些情况下,GET 是默认的 HTTP 方法。

  * 请求URL跨越多行 在 Visual Studio 的当前实现中,请求行必须在单行上。

  我们希望在未来的版本中增加对这些功能的支持。

同一文件中的多个请求

  如上所示,一个 HTTP 文件可以列出多个不同的请求。它们需要在每个请求的末尾用###分隔。

  要添加一个或多个 header,请在请求行之后(没有空行)将每个 header 添加到自己的行中。下面是一些不同的例子。

GET https://httpbin.org/get?name=Sayed?&phone=111-222-3333 HTTP/1.1
Date: Wed, 27 Apr 2023 07:28:00 GMT
###

GET https://httpbin.org/get?name=Sayed?&phone=111-222-3333
Cache-Control: max-age=604800
Age: 100
###

POST https://httpbin.org/post HTTP/1.1
Content-Type: application/json
Accept-Language: en-US,en;q=0.5

{
    "name": "sample",
    "time": "Wed, 21 Oct 2015 18:27:50 GMT"
}
###

  如果你正在调用一个 API,它可以用报头进行身份验证,你也可以在报头中指定那些。如果这样做,请注意不要向存储库提交任何私密。我们将致力于以安全的方式支持私密。

  现在我们已经了解了一些支持的语法,在下一节中,我们将列出 REST Client 具有的一些 Visual Studio 目前不支持的特性。如果您希望看到对这些功能的支持,请在下面留下评论或向团队发送一些反馈(请参阅下面的结束部分)。这个列表不分先后。有关下面列出的任何项目的更多信息,请参阅 Visual Studio Code REST Client extension.

目前在 Visual Studio HTTP 编辑器中不支持

  * Optional HTTP Method

  * Request line that spans more than one line

  * Named Requests

  * Dynamic variables

  * Environment files

  * Specify file path as body of the request

  * Mixed format for body when using multipart/form-data

  * GraphQL requests

  * cURL request

  * Copy/paste as cURL

  * Request history

  * Save response body to file

  * Certificate based authentication

  * Prompt variables

  * System variables

  * Customize response preview

  * Per-request settings

  现在我们已经介绍了 Visual Studio HTTP 编辑器支持什么,不支持什么,让我们继续讨论接下来会发生什么。

下一步

  在这篇文章中,我们讨论了开发 ASP. NET Core Web API 的端到端流程,包括 Endpoints Explorer 和 HTTP 编辑器等新工具。这是我们漫长旅程的开始,我们计划在相当长的一段时间内继续在这一领域进行投资。我们正在研究添加支持的一些特性和功能包括。下面的列表不分先后。下面的列表是我们正在探索的一些想法,我们目前还没有承诺交付所有这些功能。

  * 为请求体添加一个桩——对于需要请求体的请求,现在 Visual Studio 不会为请求体添加任何内容。我们正在寻找可以为主体添加桩的方法,以使完成该请求更容易。

  * 增加对 HTTP 文件语法的更多支持——我们只支持 REST Client 支持的语法的一个子集。我们将努力缩小差距,我们可能无法支持100%的功能,但我们将尽可能接近。这里想到的一些最重要的项目包括支持:命名请求、环境文件、支持 multipart/form-data 请求和 copy/paste as cURL.

  * 改进的响应视图——响应视图目前是非常基本的。我们计划制作一个更结构化的响应视图,以便更容易地读取来自 web 服务器的结果。

  * 在 Endpoints Explorer 中显示 HTTP 文件——目前 Endpoints Explorer 只显示解决方案中的 API 终结点。我们还希望显示解决方案中或在 Visual Studio 中打开的 HTTP 文件。这将使您能够以更简单的方式检查和浏览这些文件。

  * 请求历史——我们希望为您提供支持,以便能够查看以前发送的请求,然后重新发送这些请求。编辑以前的请求也是我们想要添加支持的功能。

  * 简化请求的用户体验——目前创建请求必须手动输入请求。有些用户更喜欢使用图形用户界面来编写请求。

  * 将响应体保存到文件中——在我们改进响应视图之后,我们希望为您添加将响应体保存到文件的功能。

  * 测试——我们希望找到一种方法来简化 Web API 项目的测试。我们还没有任何具体的计划,但这对我们来说是一个重要的领域。

  在下一节中,我将描述针对这些场景提供反馈的不同方式。如果在上面的列表中有您认为非常重要的功能,请与我们分享您的反馈。只有在您的帮助下,我们才能创造出令人难以置信的功能,您的反馈对我们非常重要。

结语

  我们在这个版本中提供的大多数更新都是受到像您这样的用户的反馈的启发。您可以通过开发者社区与我们分享反馈:通过问题报告反馈错误或问题,并分享您对新功能或改进现有功能的建议。你也可以在这里留言,或者在推特上@SayedIHashimi 联系 Sayed。

 

代码位置:https://github.com/sayedihashimi/RestaurantService

原文链接:https://devblogs.microsoft.com/visualstudio/web-api-development-in-visual-studio-2022/

 

 

posted @ 2023-07-24 00:22  MeteorSeed  阅读(3356)  评论(2编辑  收藏  举报