Nguon: Microsoft Learn · .NET 8.0

Tạo dịch vụ backend cho ứng dụng di động native với ASP.NET Core

Nguồn: Create backend services for native mobile apps with ASP.NET Core

Bởi James Montemagno

Ứng dụng di động có thể giao tiếp với các dịch vụ backend (phần sau) ASP.NET Core. Để biết hướng dẫn kết nối các web service cục bộ từ iOS simulator và Android emulator, xem Connect to local web services from Android emulators and iOS simulators.

Xem hoặc tải xuống code dịch vụ backend mẫu

Ứng dụng di động native mẫu

Hướng dẫn này minh họa cách tạo các dịch vụ backend sử dụng ASP.NET Core để hỗ trợ ứng dụng di động native. Hướng dẫn sử dụng ứng dụng .NET MAUI làm native client. Mẫu bao gồm một dự án dịch vụ ASP.NET Core Web API mà bài viết này hướng dẫn cách xây dựng.

Các tính năng

Ứng dụng TodoREST hỗ trợ liệt kê, thêm, xóa và cập nhật các mục todo. Mỗi mục có một ID, tên, ghi chú và thuộc tính chỉ ra liệu mục đó đã hoàn thành hay chưa.

View chính của các mục liệt kê tên của mỗi mục và hiển thị trạng thái hoàn thành bằng dấu tích.

Nhấn vào biểu tượng + để chuyển đến trang thêm mục.

Nhấn vào một mục trên trang chính để chuyển đến trang chỉnh sửa, nơi có thể sửa đổi tên, ghi chú và trạng thái hoàn thành của mục, hoặc xóa mục.

Để tự kiểm thử với ứng dụng ASP.NET Core được tạo trong phần tiếp theo, nếu bạn host nó online, hãy cập nhật hằng số RestUrl của ứng dụng. Nếu không, ứng dụng sẽ giao tiếp với ứng dụng ASP.NET Core được host cục bộ trên máy của bạn.

Android emulator không chạy trên máy cục bộ và sử dụng loopback IP (10.0.2.2) để giao tiếp với máy cục bộ. Dùng class DeviceInfo của .NET MAUI để phát hiện hệ điều hành mà ứng dụng đang chạy nhằm sử dụng URL chính xác.

Điều hướng đến project TodoREST và mở file Constants.cs. File Constants.cs chứa cấu hình sau.

csharp
namespace TodoREST
{
    public static class Constants
    {
        // URL của REST service
        //public static string RestUrl = "https://dotnetmauitodorest.azurewebsites.net/api/todoitems/{0}";

        // URL của REST service (Android không dùng localhost)
        // Dùng http cleartext cho triển khai cục bộ. Đổi sang https cho production
        public static string LocalhostUrl = DeviceInfo.Platform == DevicePlatform.Android ? "10.0.2.2" : "localhost";
        public static string Scheme = "https"; // hoặc http
        public static string Port = "5001";
        public static string RestUrl = $"{Scheme}://{LocalhostUrl}:{Port}/api/todoitems/{{0}}";
    }
}

Bạn có thể triển khai web service lên cloud service như Azure và cập nhật RestUrl.

Tạo dự án ASP.NET Core

Tạo một ASP.NET Core Web Application mới trong Visual Studio. Chọn template Web API. Đặt tên project là TodoAPI.

Ứng dụng nên phản hồi tất cả các request được thực hiện qua HTTPS đến port 5001.

Lưu ý: Chạy ứng dụng trực tiếp, thay vì qua IIS Express. IIS Express mặc định bỏ qua các request không phải cục bộ. Chạy dotnet run từ command prompt, hoặc chọn profile tên ứng dụng từ dropdown Debug Target trong toolbar của Visual Studio.

Thêm một lớp model (mô hình) để đại diện cho các mục todo. Đánh dấu các trường bắt buộc bằng thuộc tính [Required]:

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

Các phương thức API yêu cầu định nghĩa để làm việc với dữ liệu. Sử dụng interface ITodoRepository giống như mẫu:

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

Trong mẫu này, cài đặt repository (kho lưu trữ) chỉ dùng một collection riêng tư các mục:

csharp
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 Code",
                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);
        }
    }
}

Cấu hình cài đặt trong Program.cs:

csharp
var builder = WebApplication.CreateBuilder(args);

// Thêm services vào container.

builder.Services.AddSingleton<TodoAPI.Interfaces.ITodoRepository, TodoAPI.Services.TodoRepository>();
builder.Services.AddControllers();

var app = builder.Build();

// Cấu hình HTTP request pipeline.

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.Run();

Tạo Controller

Thêm một controller mới vào project, TodoItemsController. Nó nên kế thừa từ ControllerBase. Thêm thuộc tính Route để chỉ ra rằng controller xử lý các request đến các path bắt đầu bằng api/todoitems. Token [controller] trong route được thay thế bởi tên của controller (bỏ hậu tố Controller), và đặc biệt hữu ích cho các route toàn cục. Tìm hiểu thêm về routing.

Controller yêu cầu một ITodoRepository để hoạt động; yêu cầu một instance của kiểu này thông qua constructor của controller. Tại runtime (thời điểm chạy), instance này được cung cấp bằng cách sử dụng hỗ trợ dependency injection (chèn phụ thuộc) của framework.

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

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

API này hỗ trợ bốn HTTP verb khác nhau để thực hiện các thao tác CRUD (Create, Read, Update, Delete) trên nguồn dữ liệu. Đơn giản nhất trong số này là thao tác Read, tương ứng với HTTP GET request.

Kiểm thử API bằng curl

Bạn có thể kiểm thử API method bằng nhiều công cụ khác nhau. Trong hướng dẫn này các công cụ command-line open source (mã nguồn mở) sau được dùng:

Đọc các mục (Reading items)

Yêu cầu danh sách các mục được thực hiện bằng GET request đến phương thức List. Thuộc tính [HttpGet] trên phương thức List chỉ ra rằng action này chỉ xử lý GET request. Route cho action này là route được chỉ định trên controller.

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

Trên macOS:

Bash
curl -v -X GET 'https://localhost:5001/api/todoitems/' | jq

Trên Windows (PowerShell):

powershell
curl -v -X GET 'https://localhost:5001/api/todoitems/' | jq

Phương thức List trả về mã response 200 OK và tất cả các mục Todo, được serialize dưới dạng JSON:

output
[
  {
    "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 Code",
    "done": false
  },
  {
    "id": "ecfa6f80-3671-4911-aabe-63cc442c1ecf",
    "name": "Publish apps",
    "notes": "All app stores",
    "done": false
  }
]

Tạo mục (Creating items)

Theo quy ước, tạo các mục dữ liệu mới được ánh xạ đến HTTP POST verb. Phương thức Create có thuộc tính [HttpPost] được áp dụng và chấp nhận một instance TodoItem. Vì tham số item được truyền trong body của POST, tham số này chỉ định thuộc tính [FromBody].

Bên trong phương thức, mục được kiểm tra tính hợp lệ và sự tồn tại trước đó trong data store, và nếu không có vấn đề gì xảy ra, nó được thêm bằng repository. Kiểm tra ModelState.IsValid thực hiện model validation (xác thực model), và nên được thực hiện trong mọi API method chấp nhận input từ người dùng.

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

Mẫu sử dụng một enum chứa các mã lỗi được truyền đến mobile client:

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

Trên macOS:

Bash
curl -v -X POST 'https://localhost:5001/api/todoitems/' \
--header 'Content-Type: application/json' \
--data '{
  "id": "aaaabbbb-0000-cccc-1111-dddd2222eeee",
  "name": "A Test Item",
  "notes": "asdf",
  "done": false
}' | jq

Trên Windows (PowerShell):

powershell
curl -v -X POST 'https://localhost:5001/api/todoitems/' `
--header 'Content-Type: application/json' `
--data '{
  "id": "aaaabbbb-0000-cccc-1111-dddd2222eeee",
  "name": "A Test Item",
  "notes": "asdf",
  "done": false
}' | jq

Cập nhật mục (Updating items)

Sửa đổi bản ghi được thực hiện bằng HTTP PUT request. Ngoài sự thay đổi này, phương thức Edit gần như giống hệt Create. Nếu bản ghi không được tìm thấy, action Edit trả về response NotFound (404).

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

Trên macOS:

Bash
curl -v -X PUT 'https://localhost:5001/api/todoitems/' \
--header 'Content-Type: application/json' \
--data '{
  "id": "aaaabbbb-0000-cccc-1111-dddd2222eeee",
  "name": "A Test Item",
  "notes": "asdf",
  "done": true
}' | jq

Trên Windows (PowerShell):

powershell
curl -v -X PUT 'https://localhost:5001/api/todoitems/' `
--header 'Content-Type: application/json' `
--data '{
  "id": "aaaabbbb-0000-cccc-1111-dddd2222eeee",
  "name": "A Test Item",
  "notes": "asdf",
  "done": true
}' | jq

Phương thức này trả về response NoContent (204) khi thành công, để nhất quán với API đã có.

Xóa mục (Deleting items)

Xóa bản ghi được thực hiện bằng cách gửi DELETE request đến service và truyền ID của mục cần xóa. Như với cập nhật, các request cho các mục không tồn tại sẽ nhận response NotFound. Ngược lại, request thành công trả về response NoContent (204).

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

Trên macOS:

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

Trên Windows (PowerShell):

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

Ngăn Over-posting (gửi thừa dữ liệu)

Hiện tại, ứng dụng mẫu hiển thị toàn bộ đối tượng TodoItem. Các ứng dụng production (sản phẩm) thường giới hạn dữ liệu đầu vào và trả về bằng cách sử dụng một tập hợp con của model. Có nhiều lý do để làm điều này và bảo mật là một lý do chính. Tập hợp con của model thường được gọi là Data Transfer Object (DTO - đối tượng truyền dữ liệu), input model, hoặc view model.

DTO có thể được sử dụng để:

Để xem minh họa cách tiếp cận DTO, xem Prevent over-posting

Các quy ước Web API phổ biến

Khi bạn phát triển các dịch vụ backend cho ứng dụng, bạn sẽ muốn đưa ra một bộ quy ước hoặc chính sách nhất quán để xử lý các cross-cutting concern (mối quan tâm xuyên suốt). Ví dụ, trong service được hiển thị trước đó, các request cho các bản ghi cụ thể không được tìm thấy nhận response NotFound, thay vì response BadRequest. Tương tự, các lệnh được gửi đến service này mà truyền vào các kiểu model-bound luôn kiểm tra ModelState.IsValid và trả về BadRequest cho các kiểu model không hợp lệ.

Khi bạn đã xác định được một chính sách chung cho các API, bạn thường có thể đóng gói nó trong một filter (bộ lọc). Tìm hiểu thêm về cách đóng gói các chính sách API phổ biến trong ứng dụng ASP.NET Core MVC.