Tạo một Web API với ASP.NET Core

Tạo một dự án Web API với Controller

Tạo ứng dụng ASP.NET Core MVC trong môi trường Visual Studio 2019 Community có nhiều cách và một trong những cách nhanh nhất là dùng các template có sẵn chúng ta thực hiện các bước sau:

 1. Mở Visual Studio, chọn Create a new project, chọn ASP.NET Core Web Application và nhấn Next

2. Đặt tên dự án trong mục Project name là TodoApi và chọn vị trí lưu tại Location và nhấn Create:

3. Trong hộp thoại kế tiếp chọn API, xác nhận .NET Core, ASP.NET Core 3.1 và nhấn Create:

Kiểm tra API

Dự án đã có sẵn một API gọi là WeatherForecast. Có thể thực thi bằng cách nhấn F5 sẽ xuất hiện dữ liệu JSON như sau:

Thêm một lớp dữ liệu (model class)

Các lớp dữ liệu thể hiện các thuộc tính các đối tượng dữ liệu mà ứng dụng quản lý. Trong dự án TodoApi này, đối tượng dữ liệu của chúng ta là các công việc phải thực hiện (To-do items) bao gồm các thuộc tính Id, Name (tên công việc) và IsComplete (trạng thái phản ánh công việc đã hoàn thành hay chưa). Với đối tượng công việc này, chúng ta sẽ tạo lớp TodoItem:

public class TodoItem

{

    public long Id { get; set; }

    public string Name { get; set; }

    public bool IsComplete { get; set; }

}

Thêm lớp TodoItem đến một thư mục tên Models của dự án theo các bước sau:

1. Trong cửa sổ Solution Explorer, nhấn chuột phải tại dự án TodoApi

Chọn Add > New Folder để thêm một thư mục và đặt tên là Models

2. Tại thư mục Models nhấn chuột phải chọn Add > Class để thêm một lớp mới và đặt tên là TodoItem. Nhấn Add. Thay đổi nội dung tập tin TodoItem.cs thành:

using System;

using System.Collections.Generic;

using System.Linq;

using System.Threading.Tasks;

namespace TodoApi.Models

{

    public class TodoItem

    {

        public long Id { get; set; }

        public string Name { get; set; }

        public bool IsComplete { get; set; }

    }

}

Thêm lớp ngữ cảnh cơ sở dữ liệu (database context)

Lớp ngữ cảnh cơ sở dữ liệu là lớp chính của EF (Entity Framework) kết nối giữa lớp dữ liệu (TodoItem) và cơ sở dữ liệu. Lớp này được tạo bằng cách kế thừa từ lớp Microsoft.EntityFrameworkCore.DbContext. Các bước thêm lớp ngữ cảnh tiến hành như sau:

1. Thêm EF Core đến dự án thông qua các gói Nuget:

                – Từ Tool chọn NuGet Package Manager > Manage NuGet Packages for Solution.

                – Chọn tab Browse và nhập Microsoft.EntityFrameworkCore.InMemory trong ô tìm kiếm và Enter

                – Chọn Microsoft.EntityFrameworkCore.InMemory bên khung trái, chọn Project bên khung phải và nhấn Install. Chú ý trong mục Version là 3.0.0 đối với dự án này.

2. Thêm lớp ngữ cảnh

                – Nhấn chuột phải vào thư mục Models chọn Add > Class và đặt tên lớp là TodoContext. Nhấn Add và thay đổi nội dung tập tin TodoContext.cs như sau:

using System;

using System.Collections.Generic;

using System.Linq;

using System.Threading.Tasks;

using Microsoft.EntityFrameworkCore;

namespace TodoApi.Models

{

    public class TodoContext : DbContext

    {

        public TodoContext(DbContextOptions<TodoContext> options)

            : base(options)

        {

        }

        public DbSet<TodoItem> TodoItems { get; set; }

    }

}

Đăng ký lớp ngữ cảnh

Trong ASP.NET Core, các dịch vụ như DBContext phải được đăng ký với bộ chứa DI (Dependency Injection container). Bộ chứa này cung cấp dịch vụ đến các controller. Cập nhật nội dung Startup.cs với các using

using Microsoft.AspNetCore.Builder;

using Microsoft.AspNetCore.Hosting;

using Microsoft.Extensions.Configuration;

using Microsoft.Extensions.DependencyInjection;

using Microsoft.Extensions.Hosting;

using Microsoft.EntityFrameworkCore;

using TodoApi.Models;

Trong phương thức ConfigureServices thêm dòng mã:

public void ConfigureServices(IServiceCollection services)

        {

            services.AddDbContext<TodoContext>(opt =>

                              opt.UseInMemoryDatabase("TodoList"));

            services.AddControllers();

        }

 Dòng mã in đậm dùng để đăng ký DBContext đến bộ chứa DI và xác nhận ngữ cảnh cơ sở dữ liệu là một cơ sở dữ liệu bộ nhớ trong (in-memory database).

Scaffold một controller

Để scaffold một controller chúng ta thực hiện các bước sau:

1. Nhấn chuột phải vào thư mục Controllers chọn Add > New Scaffolded Item. Kế tiếp chọn API Controller with actions, using Entity Framework và chọn Add.

2. Trong hộp thoại Add API Controller with actions, using Entity Framework:

  • Chọn TodoItem (TodoApi.Models) trong Model class.
  • Chọn TodoContext (TodoApi.Models) trong  Data context class.
  • Chọn Add.

Tập tin mới tên TodoItemsController.cs được tạo ra.

[Route("api/[controller]")]

[ApiController]

public class TodoItemsController : ControllerBase

{

    private readonly TodoContext _context;

    public TodoItemsController(TodoContext context)

    {

        _context = context;

    }

Thuộc tính [ApiController] được sử dụng để đánh dấu cho lớp TodoItemsController xác nhận đây là controller tương ứng với yêu cầu Web API.

Đường dẫn URL cho mỗi phương thức trong lớp được xây dựng như sau:

  • Bắt đầu với thuộc tính [Route(“api/[controller]”)]
  • Thay thế [controller] trong [Route(…)] bằng tên một controller cụ thể. Trong trường hợp này, tên controller là tên lớp (hay tập tin) bỏ đi phần Controller, tức là TodoItems. Vì điều này, chúng ta thường thấy các chú thích URL trên các phương thức như GET: api/TodoItems, PUT: api/TodoItems/5,…

PostTodoItem

Đây là phương thức HTTP POST vì được đánh dấu bởi thuộc tính [HttpPost]. Phương thức nhận giá trị từ nội dung của yêu cầu HTTP.

[HttpPost]

public async Task<ActionResult<TodoItem>> PostTodoItem(TodoItem todoItem)

 {

   _context.TodoItems.Add(todoItem);

   await _context.SaveChangesAsync();

   return CreatedAtAction("GetTodoItem", new { id = todoItem.Id }, todoItem);

 }

Phương thức CreateAtAction:

  • Trả về mã HTTP 201 nếu thành công. HTTP 201 là hồi đáp chuẩn (standard response) cho một phương thức HTTP POST để tạo ra một tài nguyên mới trên máy chủ.
  • Thêm một Location header (https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Location ) đến hồi đáp. Phần header này xác định URI của một to-do item được tạo ra.
  • Tham chiếu hành động GetTodoItem để tạo ra URI của Location header. Ở đây sử dụng chuỗi tên hành động GetTodoItem – một dạng mã cứng (hard-coding).

Chúng ta sẽ sử dụng từ khóa nameof trong C# để tránh việc sử dụng mã cứng trong phương thức CreateAtAction khi tham chiếu đến hành động GetTodoItem:

return CreatedAtAction(nameof(GetTodoItem), new { id = todoItem.Id }, todoItem);

Cài đặt Postman

Trong dự án này chúng ta sẽ dùng Postman để kiểm tra Web API. Truy cập https://www.postman.com/downloads/ để tải và cài Postman.

Mở Postman, đăng nhập (tạo tài khỏan hay đăng nhập qua Google), thực hiện vài câu hỏi sẽ đến giao diện sau:

Kiểm tra phương thức PostTodoItem dùng Postman

Chúng ta có thể kiểm tra cách hoạt động của phương thức PostTodoItem thông qua Postman theo các bước sau:

1. Tạo một yêu cầu mới bằng cách nhấn vào dấu + bên phải mục Requests

Chọn phương thức POST

Và nhập URL: https://localhost:44329/api/todoitems  vào ô bên cạnh:

Chú ý cổng 44329 là cổng thực thi phụ thuộc vào các máy khác nhau. Ứng dụng trong bài viết này thực thi tại cổng 44329 (Xem kiểm tra API WeatherForecast ở trên). Kế tiếp chọn tab Body, chọn nút Raw và chọn kiểu dữ liệu là JSON:

Nhập nội dung sau trong ô trống:

{

  "name":"walk dog",

  "isComplete":true

}

Nhấn nút Send, kết quả từ khung Response bên dưới:

Nhấn nút Disable SSL Verification:

Chọn Header:

Sao chép phần VALUE bên phải của Location là URL: https://localhost:44329/api/TodoItems/1 và dán vào ô URL của Postman và đổi phương thức từ POST sang GET:

Nhấn Send.

Bây giờ chúng ta có thể kiểm tra URL https://localhost:44329/api/TodoItems/1 trên trình duyệt web

https://localhost:44329/api/TodoItems

Sẽ cho ra cùng kết quả.

Kiểm tra phương thức HTTP GET

Ở trên, sau khi thực hiện phương thức POST với Postman, chúng ta thực hiện phương thức GET sẽ cho ra cùng kết quả. Tuy nhiên, nếu ứng dụng ngừng hay bắt đầu lại thì phương thức GET sẽ không trả về giá trị nào. Nguyên nhân vì ứng dụng của chúng ta quản lý dữ liệu bộ nhớ trong sẽ mất khi ứng dụng tắt hay khởi động lại. Muốn GET trả về dự liệu, hãy thực hiện lại POST.

Để ý trong tập tin TodoItemControllers.cs có hai phương thức dùng cho HTTP GET là GetTodoItems và GetTodoItem:

// GET: api/TodoItems

        [HttpGet]

        public async Task<ActionResult<IEnumerable<TodoItem>>> GetTodoItems()

        {

            return await _context.TodoItems.ToListAsync();

        }

        // GET: api/TodoItems/5

        [HttpGet("{id}")]

        public async Task<ActionResult<TodoItem>> GetTodoItem(long id)

        {

            var todoItem = await _context.TodoItems.FindAsync(id);

            if (todoItem == null)

            {

                return NotFound();

            }

            return todoItem;

        }

Mỗi TodoItem sẽ được xác định bởi một Id duy nhất và phương thức GetTodoItem nhận item theo Id này.

Kiểu của giá trị trả về cho hai phương thức GET là kiểu  ActionResult<T> (https://docs.microsoft.com/en-us/aspnet/core/web-api/action-return-types?view=aspnetcore-3.1#actionresultt-type ). ASP.NET Core sẽ tự động chuyển dữ liệu sang dạng JSON và viết sang nội dung của thông điệp hồi đáp. Mã hồi đáp cho kiểu dữ liệu này là 200 và giả sử không có các ngoại lệ chưa được xử lý. Các ngoại lệ chưa được xử lý được chuyển sang các lỗi có mã 5xx. Trường hợp phương thức GetTodoItem không tìm thấy đối tượng TodoItem có Id được yêu cầu, mã 400 sẽ được trả về. Một vài mã trạng thái phổ biến thường gặp:

Kiểm tra phương thức PutTodoItem và DeleteTodoItem

PutTodoItem tương tự PostTodoItem ngoại trừ việc sử dụng HTTP PUT. Cách kiểm tra PutTodoItem (chọn PUT) và DeleteTodoItem (chọn DELETE) trong Postman tương tự GET và POST. Chú ý phải có dữ liệu trước khi thực hiện, nếu chưa có thì cần cung cấp dữ liệu bằng cách thực hiện POST (xem lại ở trên).

Ngăn chặn hiện tượng over-posting

Lớp dữ liệu trong ứng dụng của chúng ta là TodoItem và khi một ứng dụng được triển khai trong thực tế, một tập con của lớp dữ liệu được sử dụng thay thế với mục đích hạn chế dữ liệu đầu vào và dữ liệu trả về để tăng cường tính bảo mật cho ứng dụng. Kỹ thuật này được gọi là Data Transfer Object (DTO). DTO có thể được dùng để:

Minh họa cho trường hợp sử dụng DTO, cập nhật lớp TodoItem bao gồm một trường mới (Secret):

public class TodoItem

{

    public long Id { get; set; }

    public string Name { get; set; }

    public bool IsComplete { get; set; }

    public string Secret { get; set; }

}

Trường Secret được quản trị ứng dụng quản lý, có thể cho phép thấy hoặc không. Chúng ta phải xác nhận có thể get hay post trường này đến quản trị ứng dụng.

Tạo một lớp dữ liệu DTO:

public class TodoItemDTO

{

    public long Id { get; set; }

    public string Name { get; set; }

    public bool IsComplete { get; set; }

}

Cập nhật TodoItemsController để dùng lớp TodoItemDTO:

[HttpGet]

    public async Task<ActionResult<IEnumerable<TodoItemDTO>>> GetTodoItems()

    {

        return await _context.TodoItems

            .Select(x => ItemToDTO(x))

            .ToListAsync();

    }

    [HttpGet("{id}")]

    public async Task<ActionResult<TodoItemDTO>> GetTodoItem(long id)

    {

        var todoItem = await _context.TodoItems.FindAsync(id);

        if (todoItem == null)

        {

            return NotFound();

        }

        return ItemToDTO(todoItem);

    }

    [HttpPut("{id}")]

    public async Task<IActionResult> UpdateTodoItem(long id, TodoItemDTO todoItemDTO)

    {

        if (id != todoItemDTO.Id)

        {

            return BadRequest();

        }

        var todoItem = await _context.TodoItems.FindAsync(id);

        if (todoItem == null)

        {

            return NotFound();

        }

        todoItem.Name = todoItemDTO.Name;

        todoItem.IsComplete = todoItemDTO.IsComplete;

        try

        {

            await _context.SaveChangesAsync();

        }

        catch (DbUpdateConcurrencyException) when (!TodoItemExists(id))

        {

            return NotFound();

        }

        return NoContent();

    }

    [HttpPost]

    public async Task<ActionResult<TodoItemDTO>> CreateTodoItem(TodoItemDTO todoItemDTO)

    {

        var todoItem = new TodoItem

        {

            IsComplete = todoItemDTO.IsComplete,

            Name = todoItemDTO.Name

        };

        _context.TodoItems.Add(todoItem);

        await _context.SaveChangesAsync();

        return CreatedAtAction(

            nameof(GetTodoItem),

            new { id = todoItem.Id },

            ItemToDTO(todoItem));

    }

    [HttpDelete("{id}")]

    public async Task<IActionResult> DeleteTodoItem(long id)

    {

        var todoItem = await _context.TodoItems.FindAsync(id);

        if (todoItem == null)

        {

            return NotFound();

        }

        _context.TodoItems.Remove(todoItem);

        await _context.SaveChangesAsync();

        return NoContent();

    }

    private bool TodoItemExists(long id) =>

         _context.TodoItems.Any(e => e.Id == id);

    private static TodoItemDTO ItemToDTO(TodoItem todoItem) =>

        new TodoItemDTO

        {

            Id = todoItem.Id,

            Name = todoItem.Name,

            IsComplete = todoItem.IsComplete

        };      

}