Back to blog

Giai Đoạn 1: Bắt Đầu với ASP.NET Core & Web API

csharpdotnetaspnet-corebackendrest-api
Giai Đoạn 1: Bắt Đầu với ASP.NET Core & Web API

Series: Lộ Trình Học ASP.NET Core
Thời gian cam kết: 5–7 ngày, 1–2 giờ mỗi ngày
Yêu cầu trước: Nền tảng C#, OOP cơ bản, khái niệm REST API

Giới Thiệu

Trong giai đoạn này, bạn sẽ xây dựng REST API thực sự đầu tiên với ASP.NET Core — một API CRUD để quản lý danh sách sản phẩm. Đến cuối giai đoạn, bạn sẽ hiểu cách framework hoạt động cùng nhau và sẵn sàng kết nối cơ sở dữ liệu thực trong Giai đoạn 2.

Những gì bạn sẽ xây dựng: API Sản phẩm với CRUD đầy đủ (Create, Read, Update, Delete), validation đầu vào, tài liệu Swagger và xử lý HTTP response đúng cách.

Bạn Sẽ Học Được Gì

✅ Cài đặt .NET SDK và thiết lập môi trường phát triển
✅ Hiểu cấu trúc dự án ASP.NET Core
✅ Tạo controller, action và route
✅ Bind request body, query string và route parameter
✅ Validate đầu vào với Data Annotations và trả về lỗi
✅ Trả về HTTP status code đúng cho mọi tình huống
✅ Tài liệu hóa API với Swagger / OpenAPI
✅ Quản lý cấu hình với appsettings.json và biến môi trường

Bước 1: Cài Đặt .NET SDK

ASP.NET Core yêu cầu .NET SDK (phiên bản 8 hoặc 9).

macOS

# Tùy chọn 1: Homebrew (khuyến nghị)
brew install dotnet
 
# Tùy chọn 2: Tải bộ cài đặt từ https://dot.net

Windows

# Tùy chọn 1: winget
winget install Microsoft.DotNet.SDK.9
 
# Tùy chọn 2: Tải bộ cài đặt từ https://dot.net

Linux (Ubuntu/Debian)

sudo apt-get update
sudo apt-get install -y dotnet-sdk-9.0

Xác Nhận Cài Đặt

dotnet --version
# 9.0.x (hoặc 8.0.x)
 
dotnet --list-sdks
# Liệt kê tất cả SDK đã cài đặt

Bước 2: Chọn IDE

IDENền tảngChi phíPhù hợp nhất cho
Visual Studio 2022Windows / macOSMiễn phí (Community)Phát triển .NET đầy đủ
VS Code + C# Dev KitTất cả nền tảngMiễn phíPhát triển nhẹ
JetBrains RiderTất cả nền tảngTrả phíNgười dùng đa nền tảng nâng cao

Khuyến nghị cho người mới bắt đầu: Visual Studio 2022 trên Windows (tích hợp nhất), hoặc VS Code với extension C# Dev Kit trên macOS/Linux.

Thiết Lập VS Code

Nếu dùng VS Code, cài đặt extension này:

C# Dev Kit

C# Dev Kit bao gồm IntelliSense, debugging, test explorer và solution explorer.

Bước 3: Tạo Dự Án Đầu Tiên

# Tạo dự án Web API mới
dotnet new webapi -n ProductsApi --use-controllers
cd ProductsApi
 
# Chạy dự án
dotnet run

Mở trình duyệt tại http://localhost:5000/swagger — bạn sẽ thấy Swagger UI với endpoint WeatherForecast mẫu. Đây là điểm khởi đầu của bạn.

Hiểu Cấu Trúc Dự Án

ProductsApi/
├── Controllers/          # API controller (xử lý request)
│   └── WeatherForecastController.cs
├── Properties/
│   └── launchSettings.json   # URL và profile môi trường dev
├── appsettings.json      # Cấu hình ứng dụng
├── appsettings.Development.json  # Ghi đè cấu hình chỉ cho dev
├── ProductsApi.csproj    # File dự án (dependency, phiên bản SDK)
└── Program.cs            # Điểm khởi đầu và cấu hình ứng dụng

File Program.cs

Đây là nơi ứng dụng của bạn bắt đầu. Mở nó — nó chứa hai phần:

// 1. ĐĂNG KÝ SERVICE — cấu hình những gì ứng dụng có thể làm
var builder = WebApplication.CreateBuilder(args);
 
builder.Services.AddControllers();           // Bật MVC controller
builder.Services.AddEndpointsApiExplorer(); // Bật khám phá endpoint
builder.Services.AddSwaggerGen();           // Bật Swagger UI
 
// 2. MIDDLEWARE PIPELINE — cấu hình cách request chạy
var app = builder.Build();
 
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}
 
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();   // Kết nối route controller
 
app.Run();

Hãy nghĩ theo hai giai đoạn:

  • Giai đoạn Builder: đăng ký service (DI container)
  • Giai đoạn Pipeline: cấu hình middleware (thứ tự xử lý request)

Bước 4: Controller Đầu Tiên

Xóa WeatherForecastController.cs và WeatherForecast.cs. Tạo controller mới:

// Controllers/ProductsController.cs
using Microsoft.AspNetCore.Mvc;
 
namespace ProductsApi.Controllers;
 
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    // Lưu trữ trong bộ nhớ tạm thời (Giai đoạn 2 sẽ thêm cơ sở dữ liệu thực)
    private static readonly List<Product> _products = new()
    {
        new Product { Id = 1, Name = "Laptop", Price = 999.99m, Stock = 10 },
        new Product { Id = 2, Name = "Mouse", Price = 29.99m, Stock = 50 },
        new Product { Id = 3, Name = "Keyboard", Price = 79.99m, Stock = 30 },
    };
 
    private static int _nextId = 4;
 
    // GET api/products
    [HttpGet]
    public ActionResult<IEnumerable<Product>> GetAll()
    {
        return Ok(_products);
    }
 
    // GET api/products/1
    [HttpGet("{id}")]
    public ActionResult<Product> GetById(int id)
    {
        var product = _products.FirstOrDefault(p => p.Id == id);
        if (product is null)
            return NotFound();
 
        return Ok(product);
    }
 
    // POST api/products
    [HttpPost]
    public ActionResult<Product> Create([FromBody] CreateProductRequest request)
    {
        var product = new Product
        {
            Id = _nextId++,
            Name = request.Name,
            Price = request.Price,
            Stock = request.Stock
        };
 
        _products.Add(product);
 
        return CreatedAtAction(nameof(GetById), new { id = product.Id }, product);
    }
 
    // PUT api/products/1
    [HttpPut("{id}")]
    public ActionResult<Product> Update(int id, [FromBody] UpdateProductRequest request)
    {
        var product = _products.FirstOrDefault(p => p.Id == id);
        if (product is null)
            return NotFound();
 
        product.Name = request.Name;
        product.Price = request.Price;
        product.Stock = request.Stock;
 
        return Ok(product);
    }
 
    // DELETE api/products/1
    [HttpDelete("{id}")]
    public IActionResult Delete(int id)
    {
        var product = _products.FirstOrDefault(p => p.Id == id);
        if (product is null)
            return NotFound();
 
        _products.Remove(product);
 
        return NoContent();
    }
}

Các Class Model

Tạo thư mục Models với các class sau:

// Models/Product.cs
namespace ProductsApi.Models;
 
public class Product
{
    public int Id { get; set; }
    public string Name { get; set; } = string.Empty;
    public decimal Price { get; set; }
    public int Stock { get; set; }
}
// Models/CreateProductRequest.cs
namespace ProductsApi.Models;
 
public class CreateProductRequest
{
    public string Name { get; set; } = string.Empty;
    public decimal Price { get; set; }
    public int Stock { get; set; }
}
// Models/UpdateProductRequest.cs
namespace ProductsApi.Models;
 
public class UpdateProductRequest
{
    public string Name { get; set; } = string.Empty;
    public decimal Price { get; set; }
    public int Stock { get; set; }
}

Lưu ý: Bạn cần thêm using ProductsApi.Models; ở đầu controller, hoặc cấu hình namespace toàn cục trong file dự án.

Chạy lại dự án (dotnet run) và kiểm tra các endpoint trong Swagger.

Bước 5: Hiểu Routing

ASP.NET Core có hai cơ chế routing:

Attribute Routing (dùng trong Web API)

[Route("api/[controller]")]  // [controller] chuyển thành "products"
public class ProductsController : ControllerBase
{
    [HttpGet]              // GET /api/products
    [HttpGet("{id}")]      // GET /api/products/1
    [HttpPost]             // POST /api/products
    [HttpPut("{id}")]      // PUT /api/products/1
    [HttpDelete("{id}")]   // DELETE /api/products/1
}

Route Parameter

// Route parameter từ URL: /api/products/42
[HttpGet("{id}")]
public ActionResult<Product> GetById(int id) { ... }
 
// Query string: /api/products?page=2&pageSize=10
[HttpGet]
public ActionResult<IEnumerable<Product>> GetAll(
    [FromQuery] int page = 1,
    [FromQuery] int pageSize = 10) { ... }
 
// Request body: POST /api/products với JSON body
[HttpPost]
public ActionResult<Product> Create([FromBody] CreateProductRequest request) { ... }
 
// Route + body: PUT /api/products/42 với JSON body
[HttpPut("{id}")]
public ActionResult<Product> Update(int id, [FromBody] UpdateProductRequest request) { ... }

Các thuộc tính binding source ([FromRoute], [FromQuery], [FromBody], [FromHeader]) cho ASP.NET Core biết lấy mỗi tham số từ đâu. Khi dùng [ApiController], hầu hết các binding được suy luận tự động.

Bước 6: Validation Đầu Vào

Data Annotations

Thêm validation trực tiếp vào model request:

using System.ComponentModel.DataAnnotations;
 
public class CreateProductRequest
{
    [Required]
    [StringLength(100, MinimumLength = 2)]
    public string Name { get; set; } = string.Empty;
 
    [Required]
    [Range(0.01, 999999.99)]
    public decimal Price { get; set; }
 
    [Required]
    [Range(0, int.MaxValue)]
    public int Stock { get; set; }
}

Với [ApiController] trên controller, ASP.NET Core tự động validate request đến và trả về 400 Bad Request với chi tiết lỗi nếu validation thất bại. Bạn không cần kiểm tra ModelState.IsValid thủ công.

Validation Tùy Chỉnh

Đối với các quy tắc phức tạp hơn, implement IValidatableObject:

public class CreateProductRequest : IValidatableObject
{
    [Required]
    [StringLength(100, MinimumLength = 2)]
    public string Name { get; set; } = string.Empty;
 
    [Range(0.01, 999999.99)]
    public decimal Price { get; set; }
 
    [Range(0, int.MaxValue)]
    public int Stock { get; set; }
 
    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
        if (Price > 1000 && Stock > 100)
        {
            yield return new ValidationResult(
                "Sản phẩm giá cao không được có hơn 100 đơn vị trong kho.",
                new[] { nameof(Price), nameof(Stock) });
        }
    }
}

Định Dạng Response Lỗi Validation

Khi validation thất bại, ASP.NET Core trả về mặc định:

{
  "type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
  "title": "One or more validation errors occurred.",
  "status": 400,
  "errors": {
    "Name": ["The Name field is required."],
    "Price": ["The field Price must be between 0.01 and 999999.99."]
  }
}

Bước 7: HTTP Status Code — Response Đúng Cho Mọi Tình Huống

Trả về status code đúng là cơ bản cho API được thiết kế tốt:

// 200 OK — GET, PUT thành công
return Ok(product);
 
// 201 Created — POST thành công (với Location header)
return CreatedAtAction(nameof(GetById), new { id = product.Id }, product);
 
// 204 No Content — DELETE thành công (không có body)
return NoContent();
 
// 400 Bad Request — đầu vào không hợp lệ (tự động với [ApiController])
return BadRequest(new { error = "Thông báo lỗi tùy chỉnh" });
 
// 404 Not Found — resource không tồn tại
return NotFound();
 
// 409 Conflict — resource đã tồn tại
return Conflict(new { error = "Sản phẩm với tên này đã tồn tại." });

Bảng Tham Khảo Status Code

Tình huốngStatus CodeMethod
GET thành công200 OKOk(data)
POST thành công201 CreatedCreatedAtAction(...)
PUT thành công200 OKOk(data)
DELETE thành công204 No ContentNoContent()
Validation thất bại400 Bad RequestTự động
Chưa xác thực401 UnauthorizedUnauthorized()
Bị cấm403 ForbiddenForbid()
Không tìm thấy404 Not FoundNotFound()
Xung đột409 ConflictConflict(data)
Lỗi server500 Internal Server Error(exception middleware)

Bước 8: Tài Liệu Swagger / OpenAPI

Swagger UI được bao gồm theo mặc định trong các dự án mới. Nó tự động tạo tài liệu tương tác từ code của bạn.

Nâng Cao Swagger với XML Comment

Thêm tài liệu XML vào các action:

/// <summary>
/// Lấy tất cả sản phẩm với lọc tùy chọn.
/// </summary>
/// <param name="page">Số trang (mặc định: 1)</param>
/// <param name="pageSize">Số mục mỗi trang (mặc định: 10)</param>
/// <returns>Danh sách sản phẩm</returns>
/// <response code="200">Trả về danh sách sản phẩm</response>
[HttpGet]
[ProducesResponseType(typeof(IEnumerable<Product>), StatusCodes.Status200OK)]
public ActionResult<IEnumerable<Product>> GetAll(
    [FromQuery] int page = 1,
    [FromQuery] int pageSize = 10)
{
    var paged = _products.Skip((page - 1) * pageSize).Take(pageSize);
    return Ok(paged);
}

Bật XML comment trong .csproj:

<PropertyGroup>
  <GenerateDocumentationFile>true</GenerateDocumentationFile>
  <NoWarn>$(NoWarn);1591</NoWarn>
</PropertyGroup>

Và cấu hình Swagger sử dụng chúng trong Program.cs:

builder.Services.AddSwaggerGen(options =>
{
    options.SwaggerDoc("v1", new OpenApiInfo
    {
        Title = "Products API",
        Version = "v1",
        Description = "API mẫu để quản lý sản phẩm"
    });
 
    var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
    var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
    options.IncludeXmlComments(xmlPath);
});

Thêm using System.Reflection; và using Microsoft.OpenApi.Models; ở đầu file.

Bước 9: Quản Lý Cấu Hình

appsettings.json

Dùng cho các cài đặt ứng dụng thay đổi theo môi trường:

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "Api": {
    "MaxPageSize": 100,
    "DefaultPageSize": 10
  }
}

Đọc Cấu Hình Trong Code

// Tùy chọn 1: Trực tiếp (giá trị đơn giản)
var maxPageSize = builder.Configuration.GetValue<int>("Api:MaxPageSize");
 
// Tùy chọn 2: Options pattern (khuyến nghị cho cài đặt theo nhóm)
builder.Services.Configure<ApiSettings>(
    builder.Configuration.GetSection("Api"));
// Models/ApiSettings.cs
public class ApiSettings
{
    public int MaxPageSize { get; set; } = 100;
    public int DefaultPageSize { get; set; } = 10;
}

Inject vào controller:

public class ProductsController : ControllerBase
{
    private readonly ApiSettings _settings;
 
    public ProductsController(IOptions<ApiSettings> settings)
    {
        _settings = settings.Value;
    }
 
    [HttpGet]
    public ActionResult<IEnumerable<Product>> GetAll(
        [FromQuery] int page = 1,
        [FromQuery] int? pageSize = null)
    {
        var size = Math.Min(pageSize ?? _settings.DefaultPageSize, _settings.MaxPageSize);
        var paged = _products.Skip((page - 1) * size).Take(size);
        return Ok(paged);
    }
}

Biến Môi Trường

Ghi đè cấu hình với biến môi trường:

# Ghi đè "Api:MaxPageSize" trong appsettings.json
export Api__MaxPageSize=50   # Dùng dấu gạch dưới kép cho key lồng nhau
dotnet run

File Theo Môi Trường

appsettings.json               # Cơ sở (tất cả môi trường)
appsettings.Development.json   # Ghi đè cho dev cục bộ
appsettings.Production.json    # Ghi đè cho production

Môi trường được kiểm soát bởi biến ASPNETCORE_ENVIRONMENT (Development, Staging, Production).

Bước 10: Xử Lý Exception Toàn Cục

Đừng để exception chưa xử lý rò rỉ stack trace đến client. Thêm xử lý exception toàn cục trong Program.cs:

// Program.cs — thêm trước app.MapControllers()
app.UseExceptionHandler(errorApp =>
{
    errorApp.Run(async context =>
    {
        context.Response.StatusCode = 500;
        context.Response.ContentType = "application/json";
 
        var error = context.Features.Get<IExceptionHandlerFeature>();
        if (error != null)
        {
            await context.Response.WriteAsJsonAsync(new
            {
                type = "https://tools.ietf.org/html/rfc9110#section-15.6.1",
                title = "Đã xảy ra lỗi không mong đợi.",
                status = 500
            });
        }
    });
});

Hoặc trong .NET 8+, sử dụng interface IExceptionHandler gọn hơn:

// Infrastructure/GlobalExceptionHandler.cs
using Microsoft.AspNetCore.Diagnostics;
 
public class GlobalExceptionHandler : IExceptionHandler
{
    private readonly ILogger<GlobalExceptionHandler> _logger;
 
    public GlobalExceptionHandler(ILogger<GlobalExceptionHandler> logger)
    {
        _logger = logger;
    }
 
    public async ValueTask<bool> TryHandleAsync(
        HttpContext httpContext,
        Exception exception,
        CancellationToken cancellationToken)
    {
        _logger.LogError(exception, "Exception chưa xử lý");
 
        httpContext.Response.StatusCode = StatusCodes.Status500InternalServerError;
        await httpContext.Response.WriteAsJsonAsync(new
        {
            title = "Đã xảy ra lỗi không mong đợi.",
            status = 500
        }, cancellationToken);
 
        return true;
    }
}

Đăng ký trong Program.cs:

builder.Services.AddExceptionHandler<GlobalExceptionHandler>();
builder.Services.AddProblemDetails();
 
// Trong pipeline:
app.UseExceptionHandler();

Kết Hợp Tất Cả Lại

Đây là luồng hoàn chỉnh cho một POST request:

Kiểm Thử API

Sử Dụng Swagger UI

Swagger UI tại /swagger cho phép bạn kiểm thử tất cả endpoint trực tiếp trong trình duyệt. Click vào endpoint bất kỳ, nhấn "Try it out", điền giá trị và thực thi.

Sử Dụng curl

# GET tất cả sản phẩm
curl http://localhost:5000/api/products
 
# GET sản phẩm theo ID
curl http://localhost:5000/api/products/1
 
# POST tạo sản phẩm
curl -X POST http://localhost:5000/api/products \
  -H "Content-Type: application/json" \
  -d '{"name": "Monitor", "price": 399.99, "stock": 5}'
 
# PUT cập nhật sản phẩm
curl -X PUT http://localhost:5000/api/products/1 \
  -H "Content-Type: application/json" \
  -d '{"name": "Laptop Pro", "price": 1299.99, "stock": 8}'
 
# DELETE sản phẩm
curl -X DELETE http://localhost:5000/api/products/1

Sử Dụng VS Code REST Client

Cài extension "REST Client" và tạo file requests.http:

### Lấy tất cả sản phẩm
GET http://localhost:5000/api/products
 
### Lấy sản phẩm theo ID
GET http://localhost:5000/api/products/1
 
### Tạo sản phẩm
POST http://localhost:5000/api/products
Content-Type: application/json
 
{
  "name": "Monitor",
  "price": 399.99,
  "stock": 5
}
 
### Cập nhật sản phẩm
PUT http://localhost:5000/api/products/1
Content-Type: application/json
 
{
  "name": "Laptop Pro",
  "price": 1299.99,
  "stock": 8
}
 
### Xóa sản phẩm
DELETE http://localhost:5000/api/products/1

Các Khái Niệm Quan Trọng Cần Nhớ

[ApiController] — thêm validation tự động, suy luận binding và response problem details.

ControllerBase — class cơ sở cho Web API controller. Có tất cả phương thức helper (Ok, NotFound, CreatedAtAction, v.v.) nhưng không có hỗ trợ View.

ActionResult<T> — kiểu trả về hỗ trợ cả kết quả có type và bất kỳ IActionResult nào (như NotFound()).

Bài Tập Thực Hành

  1. Thêm Tìm Kiếm: Thêm endpoint GET /api/products/search?query=laptop lọc theo tên.
  2. Thêm Header Phân Trang: Trả về header X-Total-Count, X-Page, X-Page-Size trong endpoint GET all.
  3. Thêm Category: Thêm trường Category vào model Product và bộ lọc GET /api/products?category=electronics.
  4. Thêm Soft Delete: Thay vì xóa khỏi danh sách, thêm cờ IsDeleted và lọc nó ra khỏi response GET.
  5. Response Lỗi Tùy Chỉnh: Tùy chỉnh response 404 để trả về { "error": "Không tìm thấy sản phẩm với ID {id}." }.

Tóm Tắt

Bạn đã xây dựng REST API hoàn chỉnh với ASP.NET Core từ đầu. Đây là những gì bạn đã thực hiện:

✅ Cài đặt .NET SDK và tạo dự án
✅ Cấu trúc dự án: Program.cs, controller, model
✅ Routing với attribute routing và HTTP method attribute
✅ Model binding: route param, query string, request body
✅ Validation đầu vào với Data Annotations
✅ HTTP status code đúng cho mọi tình huống
✅ Tài liệu Swagger / OpenAPI
✅ Cấu hình với appsettings.json và biến môi trường
✅ Xử lý exception toàn cục

Trong Giai đoạn 2, bạn sẽ thay thế danh sách in-memory bằng cơ sở dữ liệu PostgreSQL thực sự sử dụng Entity Framework Core.

Series: Lộ Trình Học ASP.NET Core
Trước: Lộ Trình Học ASP.NET Core (Tổng Quan)
Tiếp Theo: Giai Đoạn 2: Truy Cập Dữ Liệu với Entity Framework Core →

📬 Đăng ký nhận bản tin

Nhận những bài viết mới nhất qua email mỗi tuần. Không spam, hủy đăng ký bất cứ lúc nào.

Chúng tôi tôn trọng quyền riêng tư của bạn. Hủy đăng ký bất cứ lúc nào.

💬 Comments

Đăng nhập để bình luận

Chúng tôi sẽ không đăng bất cứ điều gì mà không có sự cho phép của bạn.