使用 ASP.NET Core 为本机移动应用创建后端服务

作者:James Montemagno

移动应用可与 ASP.NET Core 后端服务通信。 有关从 iOS 模拟器和 Android 仿真程序连接本地 Web 服务的说明,请参阅从 iOS 模拟器和 Android 仿真程序连接到本地 Web 服务

查看或下载后端服务代码示例

本机移动应用示例

本教程演示如何使用 ASP.NET Core 创建后端服务,以支持本机移动应用。 它使用 Xamarin.Forms TodoRest 应用作为其本机客户端,其中包括 Android、iOS、Windows 的单独本机客户端。 你可以遵循链接中的教程来创建本机应用(并安装必要的免费 Xamarin 工具),以及下载 Xamarin 示例解决方案。 Xamarin 示例包含一个 ASP.NET Core Web API 服务项目,使用本文中的 ASP.NET Core 应用替换(客户端无需进行任何更改)。

在 Android 智能手机上运行的 ToDoRest 应用程序

功能

TodoREST 应用支持列出、添加、删除和更新待办事项。 每个项都有一个 ID、 Name(名称)、Notes(说明)以及一个指示该项是否已完成的属性 Done。

在上一示例中,项目的主视图列出了每个项的名称,并使用复选标记指示其是否已完成。

点击 + 图标打开“添加项”对话框:

“添加项”对话框

点击主列表屏幕上的项将打开一个编辑对话框,在其中可以修改项的名称、 说明以及是否完成,或删除项目:

“编辑项”对话框

若要使用在你计算机上运行的下一节创建的 ASP.NET Core 应用对其进行测试,请更新应用的 RestUrl 常量。

Android 模拟器不在本地计算机上运行,而是使用环回 IP (10.0.2.2) 与本地计算机进行通信。 使用 Xamarin.Essentials DeviceInfo 检测正在运行的操作系统,以使用正确的 URL。

转到 TodoREST 项目,并打开 Constants.cs 文件。 Constants.cs 文件包含以下配置。

using Xamarin.Essentials;
using Xamarin.Forms;

namespace TodoREST
{
    public static class Constants
    {
        // URL of REST service
        //public static string RestUrl = "https://YOURPROJECT.azurewebsites.net:8081/api/todoitems/{0}";

        // URL of REST service (Android does not use localhost)
        // Use http cleartext for local deployment. Change to https for production
        public static string RestUrl = DeviceInfo.Platform == DevicePlatform.Android ? "http://10.0.2.2:5000/api/todoitems/{0}" : "http://localhost:5000/api/todoitems/{0}";
    }
}

可以选择性地将 Web 服务部署到 Azure 等云服务并更新 RestUrl

创建 ASP.NET Core 项目

在 Visual Studio 中创建一个新的 ASP.NET Core Web 应用程序。 选择 Web API 模板。 将项目命名为 TodoAPI

“新建 ASP.NET Web 应用程序”对话框,其中已选中 Web API 项目模板

该应用应响应向端口 5000 发出的所有请求,包括我们移动客户端的明文 HTTP 流量。 更新 Startup.cs,使 UseHttpsRedirection 不在开发中运行:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        // For mobile apps, allow http traffic.
        app.UseHttpsRedirection();
    }

    app.UseRouting();

    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllers();
    });
}

注意

直接运行应用,而不是在 IIS Express 后面。 IIS Express 将默认忽略非本地请求。 从命令提示符处运行 dotnet run,或从 Visual Studio 工具栏中的“调试目标”下拉列表中选择应用名称配置文件。

添加一个模型类来表示待办事项。 使用 [Required] 属性标记必需字段:

using System.ComponentModel.DataAnnotations;

namespace TodoAPI.Models
{
    public class TodoItem
    {
        [Required]
        public string ID { get; set; }

        [Required]
        public string Name { get; set; }

        [Required]
        public string Notes { get; set; }

        public bool Done { get; set; }
    }
}

API 方法需要通过某种方式处理数据。 使用原始 Xamarin 示例所用的 ITodoRepository 接口:

using System.Collections.Generic;
using TodoAPI.Models;

namespace TodoAPI.Interfaces
{
    public interface ITodoRepository
    {
        bool DoesItemExist(string id);
        IEnumerable<TodoItem> All { get; }
        TodoItem Find(string id);
        void Insert(TodoItem item);
        void Update(TodoItem item);
        void Delete(string id);
    }
}

在此示例中,该实现仅使用一个专用项集合:

using System.Collections.Generic;
using System.Linq;
using TodoAPI.Interfaces;
using TodoAPI.Models;

namespace TodoAPI.Services
{
    public class TodoRepository : ITodoRepository
    {
        private List<TodoItem> _todoList;

        public TodoRepository()
        {
            InitializeData();
        }

        public IEnumerable<TodoItem> All
        {
            get { return _todoList; }
        }

        public bool DoesItemExist(string id)
        {
            return _todoList.Any(item => item.ID == id);
        }

        public TodoItem Find(string id)
        {
            return _todoList.FirstOrDefault(item => item.ID == id);
        }

        public void Insert(TodoItem item)
        {
            _todoList.Add(item);
        }

        public void Update(TodoItem item)
        {
            var todoItem = this.Find(item.ID);
            var index = _todoList.IndexOf(todoItem);
            _todoList.RemoveAt(index);
            _todoList.Insert(index, item);
        }

        public void Delete(string id)
        {
            _todoList.Remove(this.Find(id));
        }

        private void InitializeData()
        {
            _todoList = new List<TodoItem>();

            var todoItem1 = new TodoItem
            {
                ID = "6bb8a868-dba1-4f1a-93b7-24ebce87e243",
                Name = "Learn app development",
                Notes = "Take Microsoft Learn Courses",
                Done = true
            };

            var todoItem2 = new TodoItem
            {
                ID = "b94afb54-a1cb-4313-8af3-b7511551b33b",
                Name = "Develop apps",
                Notes = "Use Visual Studio and Visual Studio for Mac",
                Done = false
            };

            var todoItem3 = new TodoItem
            {
                ID = "ecfa6f80-3671-4911-aabe-63cc442c1ecf",
                Name = "Publish apps",
                Notes = "All app stores",
                Done = false,
            };

            _todoList.Add(todoItem1);
            _todoList.Add(todoItem2);
            _todoList.Add(todoItem3);
        }
    }
}

Startup.cs 中配置实现:

public void ConfigureServices(IServiceCollection services)
{
    services.AddSingleton<ITodoRepository, TodoRepository>();
    services.AddControllers();
}

创建控制器

在项目中添加新控制器 TodoItemsController。 它应该从 ControllerBase 继承。 添加 Route 属性以指示控制器处理针对以 api/todoitems 开头的路径发出的请求。 路由中的 [controller] 标记会被控制器的名称代替(省略 Controller 后缀),这对全局路由特别有用。 详细了解 路由

控制器需要 ITodoRepository 才能正常运行;通过控制器的构造函数请求该类型的实例。 在运行时,将使用框架对依赖关系注入的支持来提供此实例。

[ApiController]
[Route("api/[controller]")]
public class TodoItemsController : ControllerBase
{
    private readonly ITodoRepository _todoRepository;

    public TodoItemsController(ITodoRepository todoRepository)
    {
        _todoRepository = todoRepository;
    }

此 API 支持四个不同的 HTTP 谓词来执行对数据源的 CRUD(创建、读取、更新、删除)操作。 其中最简单的是读取操作,它对应于 HTTP GET 请求。

使用 curl 测试 API

你可以使用各种工具来测试 API 方法。 在本教程中,将使用以下开源命令行工具:

  • curl:使用各种协议(包括 HTTP 和 HTTPS)传输数据。 curl 在本教程中用于通过 HTTP 方法 GETPOSTPUTDELETE 来调用 API。
  • jq:本教程中使用的 JSON 处理器,用于格式化 JSON 数据,以便从 API 响应中轻松读取。

安装 curl 和 jq

curl 已在 macOS 中预安装,可直接在 macOS 终端应用程序中使用。 有关安装 curl 的详细信息,请参阅官方 curl 网站

jq 可以在终端中使用 Homebrew 安装:

使用以下命令安装 Homebrew(如果尚未安装):

/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

按照安装程序提供的说明进行操作。

通过以下命令使用 Homebrew 安装 jq:

brew install jq

有关 Homebrew 和 jq 安装的详细信息,请参阅 Homebrewjq

读取项目

要请求项列表,可对 List 方法使用 GET 请求。 [HttpGet] 方法的 List 属性指示此操作应仅处理 GET 请求。 此操作的路由是在控制器上指定的路由。 你不一定必须将操作名称用作路由的一部分。 你只需确保每个操作都有唯一的和明确的路由。 路由属性可以分别应用在控制器和方法级别,以此生成特定的路由。

[HttpGet]
public IActionResult List()
{
    return Ok(_todoRepository.All);
}

在终端中,调用以下 curl 命令:

curl -v -X GET 'http://localhost:5000/api/todoitems/' | jq

前面的 curl 命令包含以下组成部分:

  • -v:激活详细模式,以提供有关 HTTP 响应的详细信息,对于 API 测试和故障排除非常有用。
  • -X GET:指定对请求使用 HTTP GET 方法。 虽然 curl 通常可以推断预期的 HTTP 方法,但此选项可使其更明确。
  • 'http://localhost:5000/api/todoitems/':这是请求的目标 URL。 在此实例中,它是一个 REST API 终结点。
  • | jq:此片段与 curl 没有直接关系。 管道 | 是一个 shell 运算符,它从自身左侧的命令获取输出,并将其传输给右侧的命令。 jq 是一个命令行 JSON 处理器。 虽然 jq 不是必需的,但它使返回的 JSON 数据更易于读取。

List 方法将返回一个 200 OK 响应代码和所有 Todo 项,并将起序列化为 JSON:

[
  {
    "id": "6bb8a868-dba1-4f1a-93b7-24ebce87e243",
    "name": "Learn app development",
    "notes": "Take Microsoft Learn Courses",
    "done": true
  },
  {
    "id": "b94afb54-a1cb-4313-8af3-b7511551b33b",
    "name": "Develop apps",
    "notes": "Use Visual Studio and Visual Studio for Mac",
    "done": false
  },
  {
    "id": "ecfa6f80-3671-4911-aabe-63cc442c1ecf",
    "name": "Publish apps",
    "notes": "All app stores",
    "done": false
  }
]

创建项目

按照约定,创建新数据项将映射到 HTTP POST 谓词。 Create 方法具有应用于它的 [HttpPost] 属性,并接受 TodoItem 实例。 由于 item 参数在 POST 的正文中传递,因此该参数会指定 [FromBody] 属性。

在该方法中,会检查项的有效性和之前是否存在于数据存储,并且如果没有任何问题,则使用存储库添加。 检查 ModelState.IsValid 将执行 模型验证,应该在每个接受用户输入的 API 方法中执行此步骤。

[HttpPost]
public IActionResult Create([FromBody]TodoItem item)
{
    try
    {
        if (item == null || !ModelState.IsValid)
        {
            return BadRequest(ErrorCode.TodoItemNameAndNotesRequired.ToString());
        }
        bool itemExists = _todoRepository.DoesItemExist(item.ID);
        if (itemExists)
        {
            return StatusCode(StatusCodes.Status409Conflict, ErrorCode.TodoItemIDInUse.ToString());
        }
        _todoRepository.Insert(item);
    }
    catch (Exception)
    {
        return BadRequest(ErrorCode.CouldNotCreateItem.ToString());
    }
    return Ok(item);
}

该示例使用包含传递给移动客户端的错误代码的 enum

public enum ErrorCode
{
    TodoItemNameAndNotesRequired,
    TodoItemIDInUse,
    RecordNotFound,
    CouldNotCreateItem,
    CouldNotUpdateItem,
    CouldNotDeleteItem
}

在终端中,使用 POST 谓词调用以下 curl 命令,并在请求正文中以 JSON 格式提供新对象来测试添加新项的表现。

curl -v -X POST 'http://localhost:5000/api/todoitems/' \
--header 'Content-Type: application/json' \
--data '{
  "id": "6bb8b868-dba1-4f1a-93b7-24ebce87e243",
  "name": "A Test Item",
  "notes": "asdf",
  "done": false
}' | jq

前面的 curl 命令包括以下选项:

  • --header 'Content-Type: application/json':将 Content-Type 标头设置为 application/json,以指示请求正文包含 JSON 数据。
  • --data '{...}':发送请求正文中指定的数据。

该方法返回在响应中新建的项。

更新项目

修改记录可通过 HTTP PUT 请求完成。 除了此更改之外,Edit 方法几乎与 Create 完全相同。 如果未找到相应记录,Edit 操作将返回 NotFound (404) 响应。

[HttpPut]
public IActionResult Edit([FromBody] TodoItem item)
{
    try
    {
        if (item == null || !ModelState.IsValid)
        {
            return BadRequest(ErrorCode.TodoItemNameAndNotesRequired.ToString());
        }
        var existingItem = _todoRepository.Find(item.ID);
        if (existingItem == null)
        {
            return NotFound(ErrorCode.RecordNotFound.ToString());
        }
        _todoRepository.Update(item);
    }
    catch (Exception)
    {
        return BadRequest(ErrorCode.CouldNotUpdateItem.ToString());
    }
    return NoContent();
}

若要使用 curl 进行测试,请将谓词更改为 PUT。 在请求正文中指定要更新的对象数据。

curl -v -X PUT 'http://localhost:5000/api/todoitems/' \
--header 'Content-Type: application/json' \
--data '{
  "id": "6bb8b868-dba1-4f1a-93b7-24ebce87e243",
  "name": "A Test Item",
  "notes": "asdf",
  "done": true
}' | jq

为了与预先存在的 API 保持一致,此方法在成功时返回 NoContent (204) 响应。

删除项

删除记录可以通过向服务发出 DELETE 请求并传递要删除项的 ID 来完成。 与更新一样,请求的项不存在时会收到 NotFound 响应。 否则,成功的请求将返回 NoContent (204) 响应。

[HttpDelete("{id}")]
public IActionResult Delete(string id)
{
    try
    {
        var item = _todoRepository.Find(id);
        if (item == null)
        {
            return NotFound(ErrorCode.RecordNotFound.ToString());
        }
        _todoRepository.Delete(id);
    }
    catch (Exception)
    {
        return BadRequest(ErrorCode.CouldNotDeleteItem.ToString());
    }
    return NoContent();
}

请将 HTTP 谓词更改为 DELETE 并在 URL 末尾追加要删除的数据对象的 ID,以使用 curl 进行测试。 请求正文中不需要任何内容。

curl -v -X DELETE 'http://localhost:5000/api/todoitems/6bb8b868-dba1-4f1a-93b7-24ebce87e243'

防止过度发布

目前,示例应用公开了整个 TodoItem 对象。 生产应用通常使用模型的子集来限制输入和返回的数据。 这背后有多种原因,但安全性是主要原因。 模型的子集通常称为数据传输对象 (DTO)、输入模型或视图模型。 本文使用的是 DTO

DTO 可用于:

  • 防止过度发布。
  • 隐藏客户端不应查看的属性。
  • 省略某些属性以减少有效负载大小。
  • 平展包含嵌套对象的对象图。 对客户端而言,平展的对象图可能更方便。

要演示 DTO 方法,请参阅防止过度发布

常见的 Web API 约定

开发应用的后端服务时,需要制定一组一致的约定或策略来处理跨领域问题。 例如,在上面所示的服务中,未找到请求的特定记录时会收到 NotFound 响应,而不是 BadRequest 响应。 同样,对于此服务,传递模型绑定类型的命令始终检查 ModelState.IsValid 并为无效的模型类型返回 BadRequest

一旦为 Api 指定通用策略,一般可以将其封装在 Filter(筛选器)。 详细了解 如何封装 ASP.NET Core MVC 应用程序中的通用 API 策略

其他资源