ASP.NET Core 9: Best practices trong mở rộng khả năng ứng dụng

Trong bài này, chúng ta sẽ tìm hiểu các best practices liên quan đến ứng dụng web như thêm bộ nhớ đệm (caching), sử dụng cơ chế bất đồng bộ (asynchronous), cơ chế khả năng phục hồi (resilience) và ghi log (logging).
Chúng ta sẽ khám phá các best practices thiết yếu trong việc phát triển ứng dụng với ASP.NET Core 9, bao gồm việc sử dụng đúng các cơ chế bất đồng bộ, các yêu cầu HTTP và việc đo lường ứng dụng thông qua log.

Chúng ta sẽ tập trung vào các chủ đề sau đây:

  • Làm việc với các Best Practices trong ASP.NET Core 9
  • Cải thiện hiệu năng bằng chiến lược cache và làm cho ứng dụng có khả năng phục hồi
  • Hiểu và triển khai logging và monitoring

Yêu cầu:

Để hỗ trợ việc học trong bài này, các công cụ sau cần được cài đặt trong môi trường phát triển của bạn:

  • Docker: Docker Engine phải được cài đặt trên hệ điều hành của bạn và có container SQL Server đang chạy. Bạn có thể tìm hiểu thêm về Docker và container SQL Server trong Bài 5.
  • Postman: Công cụ này sẽ được dùng để thực thi các yêu cầu tới API của ứng dụng được phát triển.
  • Redis Insight: Công cụ này được dùng để kết nối với cơ sở dữ liệu Redis Server (https://redis.io/insight/).

Làm việc với các best practices trong ASP.NET Core 9

Giống như bất kỳ công nghệ phát triển phần mềm nào khác, ASP.NET Core không giới hạn cách chúng ta xử lý code của mình. Điều đó có nghĩa là ta có quyền tự do tạo ra các giải pháp và tiêu chuẩn mới nhằm đáp ứng các nhu cầu cụ thể.

Tuy nhiên, dựa vào các best practices không chỉ giúp ta mở rộng khả năng phát triển ứng dụng chất lượng mà còn tránh lãng phí nhiều giờ để đạt được một mục tiêu. Ta sẽ đề cập đến một số best practices cần thiết để mang lại chất lượng cao hơn cho ứng dụng của mình, bắt đầu bằng việc sử dụng đúng các yêu cầu HTTP.


Các best practices với yêu cầu HTTP

Yêu cầu HTTP là một thành phần cơ bản khi làm việc với các ứng dụng web.
Việc xử lý đúng các yêu cầu HTTP có thể ảnh hưởng đáng kể đến hiệu năng và độ tin cậy của ứng dụng.

Chúng ta đã biết về các loại động từ HTTP (HTTP verbs)mã trạng thái (status codes) trong bài https://minhphien.com/asp-net-core-9-tao-web-api-danh-cho-nguoi-moi-bat-dau/
Tuy nhiên, mỗi phương thức HTTP được cung cấp bởi ứng dụng phải được xử lý đúng cách để tránh sai lệch hoặc lỗ hổng bảo mật.

Hơn nữa, cách mà các yêu cầu HTTP được thực hiện ảnh hưởng trực tiếp đến trải nghiệm của người dùng hoặc các hệ thống tiêu thụ dịch vụ của bạn.

Hãy cùng tìm hiểu một số best practices liên quan đến các yêu cầu HTTP.


Xác thực và làm sạch đầu vào

Luôn luôn xác thực (validate) và làm sạch (sanitize) dữ liệu đầu vào để ngăn chặn các lỗ hổng bảo mật như SQL InjectionCross-site scripting (XSS).

XSS

XSS là một lỗ hổng bảo mật nơi kẻ tấn công chèn các đoạn script vào trang web.
Tìm hiểu thêm tại: https://learn.microsoft.com/en-us/aspnet/core/security/cross-site-scripting?view=aspnetcore-9.0

Hãy xem xét tình huống khi người dùng gửi input form chứa tên người dùng.
Để ngăn chặn dữ liệu độc hại được xử lý, bạn nên xác thực đầu vào để đảm bảo nó đúng định dạng mong đợi và làm sạch nó để loại bỏ bất kỳ nội dung nguy hiểm nào:

public IActionResult Submit(string username)
{
    if (string.IsNullOrEmpty(username))
    {
        return BadRequest("Username is required.");
    }
    username = HttpUtility.HtmlEncode(username);
    // Tiếp tục xử lý username
    return Ok();
}

Đoạn code trên minh họa việc xác thực đơn giản cho tham số username.
Phương thức HttpUtility.HtmlEncode(username) được dùng để chuyển các ký tự như <, >, &… thành dạng HTML an toàn.


Sử dụng các phương thức bất đồng bộ (Asynchronous)

Trong quá trình thực thi của một yêu cầu HTTP, ta nên tránh thực hiện các hành động xử lý theo cách đồng bộ (synchronous). Nếu không, điều này có thể làm giảm trải nghiệm người dùng và gây ra các vấn đề:

  • Thread blocking: phương thức đồng bộ sẽ block luồng trong khi chờ các thao tác I/O (như truy vấn cơ sở dữ liệu, truy cập file hoặc yêu cầu mạng).
    Trong ứng dụng ASP.NET Core, thread pool là tài nguyên có giới hạn.
  • Thread pool exhaustion: khi ứng dụng phụ thuộc nhiều vào các phương thức đồng bộ, thread pool có thể bị cạn kiệt, đặc biệt khi tải cao – tức là khi tất cả các luồng có sẵn đều bị chặn, không còn luồng nào xử lý yêu cầu mới.

Vì vậy, việc dùng các phương thức bất đồng bộ là thực hành được khuyến nghị để cải thiện hiệu năng và khả năng mở rộng.
Ví dụ: khi dùng đối tượng HttpClient để gửi yêu cầu API, hãy dùng HttpClient.SendAsync() thay vì HttpClient.Send().

Lập trình bất đồng bộ cho phép ứng dụng xử lý nhiều tác vụ cùng lúc mà không phải chờ từng tác vụ hoàn tất — tương tự như một đầu bếp bận rộn có thể chuẩn bị nhiều món ăn song song thay vì đợi từng món chín xong mới bắt đầu món tiếp theo.

Ta sẽ nói chi tiết hơn về việc dùng bất đồng bộ trong phần “Asynchronous requests and I/O optimization”. Trước hết, cần hiểu thêm một best practices khác liên quan đến HTTP: caching và nén dữ liệu.


Caching và nén (Compression)

Các yêu cầu HTTP có nhiều thuộc tính như headerbody.
Trong quá trình giao tiếp giữa ứng dụng và backend, các thông tin này được truyền đi, và phần header được cả client (trình duyệt) lẫn backend sử dụng.

Có nhiều loại HTTP header khác nhau, trong đó có các header liên quan đến cache và nén.

Bằng cách tận dụng cachingresponse compression, ta có thể giảm băng thông và cải thiện thời gian tải.
Trình duyệt cũng nhận biết các header này, giúp tránh việc gửi lại những yêu cầu không cần thiết đến server.

Hãy xem ví dụ code trích từ Program.cs:

// Add services to the container.
builder.Services.AddResponseCaching();
app.UseResponseCaching();
app.Use(async (context, next) => {
  context.Response.GetTypedHeaders().CacheControl =
    new Microsoft.Net.Http.Headers.CacheControlHeaderValue
    {
      Public = true, MaxAge = TimeSpan.FromMinutes(10)
    };
    await next();
});

Giải thích:

  • app.UseResponseCaching thêm middleware caching vào pipeline.
  • Middleware này kiểm tra các header Cache-Control để quyết định có thể cache phản hồi hay không.
  • Nếu phản hồi được phép cache, nó lưu lại trong bộ nhớ tạm. Các yêu cầu sau cùng tiêu chí sẽ lấy từ cache thay vì xử lý lại.

Cơ chế này giảm thời gian xử lý và tải trên server vì phản hồi được lấy trực tiếp từ cache.

Phần app.Use(async (context, next)...) thêm header Cache-Control với thời gian tồn tại (MaxAge).
Điều này giúp client biết phản hồi nên được lưu trong cache bao lâu.

Cache được quản lý trong bộ nhớ của ứng dụng, nên không nên lưu quá lâu vì sẽ tốn tài nguyên, nhưng vẫn rất hữu ích khi dùng đúng cách.
Ta sẽ đi sâu hơn về cache trong phần tiếp theo.

Để tăng tốc hơn nữa, ta có thể tự động nén phản hồi chỉ với vài dòng code.

Cài gói cần thiết:

dotnet add package Microsoft.AspNetCore.ResponseCompression

Sau khi thêm, cấu hình trong Program.cs như sau:

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddResponseCompression(options =>
{
    options.EnableForHttps = true; // Bật nén cho HTTPS
    options.Providers.Add<GzipCompressionProvider>(); // Thêm Gzip
    options.Providers.Add<BrotliCompressionProvider>(); // Thêm Brotli
});

builder.Services.Configure<GzipCompressionProviderOptions>(options =>
{
    options.Level = System.IO.Compression.CompressionLevel.Fastest;
    // Thiết lập mức độ nén
});

builder.Services.AddResponseCaching();

var app = builder.Build();

app.UseResponseCompression(); // Nén phản hồi
app.UseResponseCaching();     // Cache phản hồi

app.Use(async (context, next) =>
{
  context.Response.GetTypedHeaders().CacheControl =
    new Microsoft.Net.Http.Headers.CacheControlHeaderValue
  {
    Public = true,
    MaxAge = TimeSpan.FromMinutes(10)
  };
  await next();
});

app.Run();

Lưu ý thứ tự middleware:
Khi kết hợp caching và compression, hãy đặt middleware compression trước caching để phản hồi được nén rồi mới lưu vào cache.

Nhờ vậy, ứng dụng giảm kích thước phản hồi, tải nhanh hơn và sử dụng băng thông hiệu quả hơn.


Các yêu cầu bất đồng bộ và tối ưu I/O

Lập trình bất đồng bộ là yếu tố cơ bản của phát triển web hiện đại, giúp thực hiện các tác vụ không chặn (non-blocking), cải thiện độ phản hồi và khả năng mở rộng.

Độ phức tạp của lập trình bất đồng bộ được C# trừu tượng hóa thông qua các từ khóa và thư viện có sẵn, giúp ứng dụng mạnh mẽ hơn.
Để hiểu rõ tầm quan trọng của cơ chế này, hãy tưởng tượng:

Bạn đang xếp hàng mua cà phê. Nếu nhân viên pha chế phải chờ từng ly xong mới bắt đầu ly tiếp theo, hàng sẽ di chuyển rất chậm.
Thay vào đó, họ chuẩn bị nhiều ly song song.
Lập trình bất đồng bộ cũng hoạt động như vậy — không chặn khi đang chờ.

Ứng dụng web có thể nhận hàng trăm yêu cầu cùng lúc. ASP.NET Core 9 được tối ưu để xử lý yêu cầu và bộ nhớ hiệu quả.
Tuy nhiên, nếu ta chọn cách đồng bộ, ứng dụng có thể gặp vấn đề.
Hãy xem cách triển khai phương thức bất đồng bộ.


Sử dụng từ khóa asyncawait

Trong C#, hai từ khóa asyncawait cho phép viết code bất đồng bộ dễ đọc, dễ bảo trì.

Ví dụ:

public async Task<IActionResult> GetDataAsync()
{
    var data = await _dataService.GetDataAsync();
    return Ok(data);
}

Giải thích:

  • async: đánh dấu phương thức là bất đồng bộ.
  • Task<IActionResult>: phương thức trả về một tác vụ (Task), đại diện cho quá trình xử lý bất đồng bộ.
  • await: tạm dừng thực thi cho đến khi GetDataAsync() hoàn tất mà không khóa luồng.
  • _dataService.GetDataAsync(): gọi phương thức bất đồng bộ khác để lấy dữ liệu.

C# có nhiều phương thức bất đồng bộ theo quy ước đặt hậu tố Async, ví dụ: SaveChangesAsync(), ToListAsync().

Tài liệu thêm:
Asynchronous programming in C# (Microsoft Learn)

Khi kết hợp với các tính năng của ASP.NET Core 9, ta có thể triển khai yêu cầu bất đồng bộ dễ dàng và hiệu quả.

Ví dụ với Entity Framework Core:

public async Task<List<Customer>> GetCustomersAsync()
{
    return await _dbContext.Customers.ToListAsync();
}

Truy cập dữ liệu bất đồng bộ cho phép ứng dụng thực hiện các tác vụ khác trong khi chờ dữ liệu từ cơ sở dữ liệu — tối ưu tài nguyên và phản hồi nhanh hơn.

Cải thiện hiệu năng với chiến lược cache và làm cho ứng dụng có khả năng phục hồi

Trong phần các thực hành tốt với yêu cầu HTTP ở mục Làm việc với ASP.NET Core 9 best practices, chúng ta đã tìm hiểu về một số cơ chế giúp cải thiện đáng kể ứng dụng của mình. Một vài phương pháp tiếp cận đã được nhắc đến, bao gồm giới thiệu ngắn gọn về việc sử dụng cache.

Để mở rộng kiến thức và bổ sung kỹ thuật cho mô hình phát triển ứng dụng mạnh mẽ hơn, chúng ta sẽ đi sâu vào việc sử dụng chiến lược cache và cách làm cho ứng dụng có khả năng phục hồi (resilient) — đây là một yêu cầu cơ bản đối với các giải pháp hiện đại.

Trước hết, hãy hiểu về các loại chiến lược cache khác nhau.


Các chiến lược cache

Cache là một kỹ thuật mạnh mẽ giúp cải thiện hiệu năng ứng dụng bằng cách lưu trữ tạm thời dữ liệu được truy cập thường xuyên.
Nhờ đó, ứng dụng giảm được số lần phải lấy lại dữ liệu từ nguồn gốc ban đầu.

Trong phần Caching và compression, chúng ta đã xem ví dụ code cho phép ứng dụng quản lý cache thông qua middleware của ASP.NET Core 9.
Đối với trường hợp đó, chiến lược cache trong bộ nhớ (in-memory cache) được sử dụng — dữ liệu được lưu trong bộ nhớ để truy cập nhanh.
Cách này phù hợp cho các tập dữ liệu nhỏ hoặc trung bình được truy cập thường xuyên.

Tuy nhiên, với những ứng dụng lớn hơn, ta cần một chiến lược khác gọi là distributed cache (bộ nhớ đệm phân tán).

Bộ nhớ đệm phân tán sử dụng các công cụ chuyên biệt, ví dụ Redis, để quản lý cache theo mô hình phân tán.

Redis là một công nghệ mạnh mẽ, phù hợp cho các bộ dữ liệu lớn hoặc khi ứng dụng chạy trong môi trường phân tán.


Redis là gì?

Remote Dictionary Server (Redis) là một hệ thống lưu trữ cấu trúc dữ liệu trong bộ nhớ, mã nguồn mở.
Redis nổi tiếng nhờ hiệu năng cao, tính linh hoạt, và hỗ trợ nhiều kiểu cấu trúc dữ liệu khác nhau.

Redis lưu dữ liệu trong RAM, giúp nó nhanh hơn rất nhiều so với cơ sở dữ liệu lưu trên đĩa, và cũng hỗ trợ cơ chế lưu định kỳ xuống đĩa để đảm bảo dữ liệu tồn tại.

Mô hình lưu trữ của Redis là key/value, hỗ trợ các cấu trúc như:

  • strings
  • hashes
  • lists
  • sets
  • sorted sets
  • bitmaps
  • HyperLogLogs
  • chỉ mục không gian địa lý (geospatial indexes)

Nhờ sự linh hoạt đó, Redis được ứng dụng trong nhiều tình huống khác nhau.

Redis hiện được dùng rộng rãi trong các ứng dụng hiện đại, đặc biệt trong môi trường điện toán đám mây.
Nếu muốn tìm hiểu thêm, xem tại: https://redis.io/.


Nhiều ứng dụng hiện đại — đặc biệt là ứng dụng chạy trên cloud — sử dụng Redis như một giải pháp cache phân tán và Redis được tích hợp hoàn chỉnh với ASP.NET Core 9.

Để hiểu rõ cách Redis hoạt động khi tích hợp vào ASP.NET Core 9, hãy cùng triển khai một ứng dụng mẫu.

Hãy chú ý đến các yêu cầu đã nêu trong phần Yêu cầu kỹ thuật ở đầu bài.
Giờ ta sẽ học cách tích hợp Redis vào ứng dụng của mình.


Tích hợp Redis vào ứng dụng

  1. Mở terminal tại thư mục bất kỳ và thực hiện các bước sau:

Tạo dự án ASP.NET Core 9 mới:

dotnet new webapi -n DistributedCacheExample
cd DistributedCacheExample

Thêm gói Redis cache:

dotnet add package Microsoft.Extensions.Caching.StackExchangeRedis
  1. Mở dự án trong Visual Studio Code:
code .
  1. Mở file appsettings.json và thay nội dung bằng:
{
  "ConnectionStrings": {
    "Redis": "localhost:6379"
  }
}

Đoạn JSON trên định nghĩa chuỗi kết nối đến máy chủ Redis mà ta sẽ tạo ở bước sau.

  1. Mở file Program.cs và thay toàn bộ nội dung bằng:
using Microsoft.Extensions.Caching.Distributed;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

// Configure Redis distributed cache
builder.Services.AddStackExchangeRedisCache(options =>
{
  options.Configuration = builder.Configuration.GetConnectionString("Redis");
  options.InstanceName = "myPrefix_";
});

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseAuthorization();
app.MapControllers();
app.Run();

Bạn hẳn đã quen với hầu hết các phần trong đoạn code trên.
Phương thức builder.Services.AddStackExchangeRedisCache thêm các đối tượng cần thiết — thuộc thư viện Microsoft.Extensions.Caching.StackExchangeRedis — để cấu hình và quản lý cache trong container DI.

Có hai cấu hình chính:

  • options.Configuration: địa chỉ kết nối tới máy chủ Redis
  • options.InstanceName: tham số tùy chọn, định nghĩa tiền tố cho các khóa trong cache

Nền tảng ứng dụng đã sẵn sàng. Tiếp theo, ta sẽ tạo một controller để tương tác với Redis.


Làm việc với cache trong controller

Trong Visual Studio Code, thực hiện:

  1. Nếu chưa có, tạo thư mục Controllers trong gốc dự án.
  2. Thêm lớp CacheController vào thư mục đó.
  3. Thay toàn bộ nội dung file bằng code sau:
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Caching.Distributed;
using System.Text.Json;
using System.Text;

namespace DistributedCacheExample.Controllers;

[ApiController]
[Route("api/[controller]")]
public class CacheController : ControllerBase
{
    private readonly IDistributedCache _cache;

    public CacheController(IDistributedCache cache)
    {
        _cache = cache;
    }

    [HttpGet("{key}")]
    public async Task<IActionResult> Get(string key)
    {
        var cachedData = await _cache.GetStringAsync(key);
        if (string.IsNullOrEmpty(cachedData))
        {
            return NotFound();
        }
        var data = JsonSerializer.Deserialize<MyData>(cachedData);
        return Ok(data);
    }

    [HttpPost]
    public async Task<IActionResult> Post([FromBody] MyData data)
    {
        var cacheKey = data.Key;
        var serializedData = JsonSerializer.Serialize(data);

        var options = new DistributedCacheEntryOptions()
            .SetSlidingExpiration(TimeSpan.FromMinutes(5))
            .SetAbsoluteExpiration(TimeSpan.FromHours(1));

        await _cache.SetStringAsync(cacheKey, serializedData, options);
        return CreatedAtAction(nameof(Get), new { key = cacheKey }, data);
    }
}

public class MyData
{
    public string Key { get; set; }
    public string Value { get; set; }
}

Đoạn code trên tạo ra API Cache với hai phương thức GETPOST.

Giải thích các điểm chính:

  • Microsoft.Extensions.Caching.Distributed: namespace chứa các dependency để làm việc với cache.
  • IDistributedCache _cache: thuộc tính riêng trong class, đóng vai trò giao tiếp với cache.
  • Hàm khởi tạo CacheController(IDistributedCache cache) dùng dependency injection để khởi tạo _cache.
  • Trong Get(): gọi _cache.GetStringAsync(key) để lấy dữ liệu từ Redis, trả về 404 nếu không có.
  • Trong Post(): nhận đối tượng MyData, dùng Key làm khóa, tuần tự hóa (Serialize) thành JSON, cấu hình thời gian hết hạn và lưu vào Redis.

SetSlidingExpiration và SetAbsoluteExpiration:

  • SetSlidingExpiration(TimeSpan.FromMinutes(5)) xác định thời gian dữ liệu bị xóa nếu không được truy cập lại trong vòng 5 phút. Mỗi lần truy cập, thời gian này được làm mới.
  • SetAbsoluteExpiration(TimeSpan.FromHours(1)) xác định dữ liệu sẽ bị xóa sau 1 giờ, bất kể được truy cập bao nhiêu lần.

Chạy Redis server bằng Docker

Tại thư mục dự án, mở terminal và chạy:

docker run --name redis -d -p 6379:6379 redis

Nếu đây là lần đầu bạn chạy Redis, hãy chờ tải về hoàn tất.
Sau đó, Redis sẽ khởi động.

Chạy ứng dụng:

dotnet run

Mở Postman, tạo yêu cầu mới:

  • Chọn File | New Tab
  • Đặt loại yêu cầu là GET
  • Trong ô URL, nhập đường dẫn của API (hiển thị trong terminal khi chạy ứng dụng), thêm hậu tố /api/Cache/DataInCache.

Cấu hình yêu cầu API trên Postman

Cổng 5277 trong ví dụ là cổng ứng dụng khi chạy API, giá trị này có thể khác nhau tùy máy.
DataInCache là khóa cần lấy từ cache.

Khi nhấn Send, bạn sẽ thấy phản hồi 404 Not Found.

Yêu cầu dữ liệu trong cache

Điều này đúng, vì cache chưa có dữ liệu tương ứng.

Tiếp theo, tạo một yêu cầu POST mới:

  • URL: /api/Cache
  • Tab Body → chọn raw → kiểu JSON, thêm nội dung:
{
  "key": "DataInCache",
  "value": "Value in cache"
}

Cấu hình yêu cầu POST

Nhấn Send, bạn sẽ nhận phản hồi HTTP 201 Created, nghĩa là dữ liệu đã được lưu vào cache Redis.

Sau đó, quay lại tab GET cũ, gửi lại yêu cầu — bạn sẽ thấy phản hồi 200 OK cùng JSON chứa dữ liệu vừa lưu.


Cấu hình Redis Insight

Để xem trực quan dữ liệu cache, ta có thể kết nối Redis Insight với container Redis đang chạy.

  1. Mở Redis Insight, chọn Add connection details manually.
  2. Giữ mặc định:
    • Host: localhost
    • Port: 6379
    • Các tham số khác có thể bỏ qua (trong môi trường thật, có thể cần user và password).
  3. Nhấn + Add Redis database.

Redis Insight sẽ hiển thị danh sách kết nối.

Redis cache được kết nối trong Redis Insight

Nhấn vào kết nối, rồi nhấn biểu tượng kính lúp 🔍 để xem dữ liệu có trong cache.

Danh sách dữ liệu trong cache

Nếu không thấy dữ liệu, có thể khóa đã hết hạn — hãy gửi lại yêu cầu POST để thêm mới và kiểm tra lại.


Ví dụ này giúp ta hiểu cách lưu và truy xuất dữ liệu cache trong bộ nhớ.
Trong trường hợp này, Redis đóng vai trò máy chủ lưu trữ dữ liệu tạm trong bộ nhớ RAM.

Trong các tình huống thực tế, cache thường được dùng kết hợp với cơ sở dữ liệu:
Khi có yêu cầu, ứng dụng sẽ kiểm tra cache trước. Nếu dữ liệu đã có, không cần truy vấn database, giúp tiết kiệm tài nguyên và tăng tốc phản hồi.

Như ta thấy, cache là công cụ mạnh mẽ giúp ứng dụng hoạt động nhanh và sẵn sàng hơn.

Giờ khi đã hiểu cách lấy thông tin nhanh từ cache, hãy chuyển sang làm cho ứng dụng có khả năng phục hồi tốt hơn.

Cơ chế khả năng phục hồi (Resilience mechanisms)

Để xây dựng các ứng dụng mạnh mẽ, điều thiết yếu là phải triển khai các cơ chế khả năng phục hồi (resilience mechanisms) có thể xử lý lỗi tạm thời và đảm bảo tính sẵn sàng liên tục.

Hãy hình dung các cơ chế phục hồi như lưới an toàn giúp ứng dụng không bị sụp đổ khi có sự cố. Chúng cho phép ứng dụng khôi phục từ những lỗi bất ngờ và vẫn duy trì trải nghiệm mượt mà cho người dùng.

Các chiến lược phục hồi phổ biến nhất gồm:

  • Retry pattern (mẫu thử lại): tự động thử lại một thao tác bị lỗi một số lần nhất định trước khi bỏ cuộc. Hữu ích khi gặp lỗi tạm thời.
  • Circuit breaker pattern (mẫu ngắt mạch): ngăn ứng dụng tiếp tục gọi một thao tác có khả năng thất bại cao. Nó dừng dòng yêu cầu đến dịch vụ khi phát hiện lỗi, cho phép hệ thống có thời gian khôi phục.

Để triển khai các mẫu này trong ứng dụng, ta sẽ sử dụng một thư viện có tên Polly.


Polly

Polly là một thư viện thuộc .NET Foundation, được dùng để thêm các tính năng phục hồi cho ứng dụng.
Thư viện này được cộng đồng mã nguồn mở duy trì và cập nhật thường xuyên, được sử dụng rộng rãi trong môi trường sản xuất.
Tìm hiểu thêm tại: https://github.com/App-vNext/Polly

Để sử dụng Polly, chỉ cần thêm vào dự án bằng lệnh:

dotnet add package Polly.Core

Ví dụ triển khai chiến lược Retry

var retryPolicy = Policy.Handle<Exception>().RetryAsync(3);

public async Task<IActionResult> GetDataWithRetryAsync()
{
    return await retryPolicy.ExecuteAsync(async () =>
    {
        var data = await _dataService.GetDataAsync();
        return Ok(data);
    });
}

Phân tích:

  • Policy.Handle<Exception>().RetryAsync(3) tạo một chính sách thử lại tối đa 3 lần khi gặp ngoại lệ.
  • retryPolicy.ExecuteAsync(...) thực thi hành động nằm trong phạm vi chính sách. Nếu lỗi xảy ra, Polly sẽ tự động gọi lại thao tác cho đến khi vượt quá số lần thử.

Chiến lược retry thường được dùng khi ứng dụng gọi các API bên ngoài – nơi có thể xảy ra lỗi tạm thời do mạng hoặc dịch vụ không ổn định. Việc thử lại giúp tăng độ tin cậy khi gặp sự cố thoáng qua.


Ví dụ triển khai chiến lược Circuit Breaker

var circuitBreakerPolicy = Policy.Handle<Exception>()
  .CircuitBreakerAsync(
    3, // Số lỗi liên tiếp trước khi ngắt mạch
    TimeSpan.FromMinutes(1) // Thời gian ngắt mạch
);

public async Task<IActionResult> GetDataWithCircuitBreakerAsync()
{
    return await circuitBreakerPolicy.ExecuteAsync(async () =>
    {
        var data = await _dataService.GetDataAsync();
        return Ok(data);
    });
}

Giải thích:

  • CircuitBreakerAsync: tạo chính sách ngắt mạch bất đồng bộ.
  • Sau 3 lỗi liên tiếp, mạch sẽ mở (open) – mọi yêu cầu tiếp theo đều bị từ chối trong 1 phút (TimeSpan.FromMinutes(1)).
  • Khi hết thời gian, mạch chuyển sang trạng thái half-open: cho phép một vài yêu cầu thử nghiệm.
    • Nếu thành công, mạch đóng lại (closed) và hoạt động bình thường.
    • Nếu thất bại, mạch mở lại (open) để chặn yêu cầu.

Phương thức ExecuteAsync thực thi đoạn code trong phạm vi chính sách circuit breaker.
Nếu _dataService.GetDataAsync() thành công → trả về dữ liệu (HTTP 200).
Nếu ném ngoại lệ → circuit breaker xử lý và quyết định trạng thái của mạch.

Chiến lược này ngăn hệ thống bị quá tải bởi các lỗi lặp lại, giúp các dịch vụ phụ thuộc có thời gian phục hồi.


So sánh Circuit Breaker và Retry

Khía cạnhCircuit BreakerRetry
Mục đíchNgăn quá tải dịch vụ đang gặp lỗiXử lý lỗi tạm thời bằng cách thử lại
Hành viDừng yêu cầu sau số lần lỗi liên tiếp, mở mạch trong thời gian xác địnhThử lại thao tác nhiều lần với độ trễ giữa các lần
Trạng tháiClosed, Open, Half-OpenKhông có trạng thái, chỉ lặp lại
Xử lý lỗiThất bại ngay khi mạch mởChỉ thất bại sau khi thử lại hết số lần
Khi nào dùngKhi muốn bảo vệ hệ thống khỏi lỗi liên tụcKhi lỗi tạm thời có thể tự khắc phục
Độ phức tạpCao hơn, có trạng thái và cần giám sátĐơn giản, chỉ có logic thử lại
Phản hồi với người dùngThất bại ngay lập tức khi mạch mởPhản hồi chậm hơn, sau khi thử lại xong

Mục tiêu của Circuit Breaker và Retry

Trên thực tế, hai mẫu này thường được kết hợp để đạt hiệu quả cao nhất:
– Retry xử lý lỗi tạm thời;
– Circuit Breaker ngăn hệ thống tiếp tục gọi khi lỗi kéo dài.

Polly còn cung cấp nhiều cơ chế phục hồi khác có thể kết hợp để xây dựng ứng dụng mạnh mẽ và đáng tin cậy, đặc biệt trong môi trường cloud.


Ngay cả khi đã áp dụng nhiều cơ chế tránh lỗi, lỗi vẫn có thể xảy ra.
Vì vậy, chúng ta cần có thông tin đầy đủ để phát hiện, chẩn đoán và khắc phục.
Đây là lý do tại sao logging trở thành phần quan trọng – ASP.NET Core 9 cung cấp hệ thống logging mạnh mẽ, mà ta sẽ tìm hiểu ngay sau đây.


Hiểu và triển khai logging và monitoring

Sau khi đã tối ưu hiệu năng và khả năng phục hồi, bước tiếp theo là logging và monitoring – hai thực hành thiết yếu để duy trì và khắc phục sự cố trong ứng dụng ASP.NET Core 9.


Giới thiệu về logging và monitoring

Logging và monitoring rất quan trọng để hiểu hành vi của ứng dụng, chẩn đoán vấn đề, và đảm bảo nó vận hành trơn tru.
Log cung cấp khả năng quan sát quá trình hoạt động và giúp phát hiện sớm các bất thường.

Hãy tưởng tượng: logging giống như việc ghi nhật ký, còn monitoring giống như gắn camera giám sát.
Nhật ký giúp bạn biết chuyện gì đã xảy ra trong quá khứ, còn camera giúp bạn thấy tình hình hiện tại – cả hai đều giúp duy trì sự an toàn và trật tự.


Logging với ILogger

.NET cung cấp các abstraction (trừu tượng hóa) cho phép ứng dụng ASP.NET Core 9 xử lý nhiều chiến lược ghi log khác nhau.
Hai interface quan trọng là ILoggerILoggerFactory, nằm trong namespace Microsoft.Extensions.Logging.

ILogger cho phép ghi lại thông tin về hoạt động của ứng dụng ở nhiều mức độ chi tiết khác nhau:

  • Trace: thông tin chi tiết nhất, dùng khi chẩn đoán vấn đề.
  • Debug: thông tin phục vụ mục đích debug.
  • Information: mô tả tiến trình hoạt động bình thường.
  • Warning: cảnh báo, tình huống có thể gây lỗi.
  • Error: lỗi khiến một chức năng không thực thi được.
  • Critical: lỗi nghiêm trọng làm ứng dụng ngừng hoạt động.

Các phương thức chính của ILogger

  • Phương thức ghi log tổng quát:
Log<TState>(LogLevel logLevel, EventId eventId, TState state,
             Exception exception, Func<TState, Exception, string> formatter)

Cho phép xác định mức log, ID sự kiện, dữ liệu trạng thái, ngoại lệ và hàm định dạng.

  • Các phương thức tiện lợi:
LogTrace(string message)
LogDebug(string message)
LogInformation(string message)
LogWarning(string message)
LogError(string message)
LogCritical(string message)
  • Phương thức phạm vi (Scope):
BeginScope<TState>(TState state)

Dùng để nhóm các log liên quan đến cùng một ngữ cảnh.


Ví dụ sử dụng ILogger

public class MyService
{
    private readonly ILogger<MyService> _logger;

    public MyService(ILogger<MyService> logger)
    {
        _logger = logger;
    }

    public void DoWork()
    {
        _logger.LogInformation("Starting work.");
        try
        {
            // Thực thi công việc
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "An error occurred while doing work.");
        }
        _logger.LogInformation("Finished work.");
    }
}

Giải thích:

  • ILogger<MyService>: logger gắn với class MyService, giúp dễ nhận biết nguồn log.
  • LogInformationLogError được dùng để ghi thông tin và lỗi.
  • Logger được tiêm qua Dependency Injection, nên có thể cấu hình tập trung trong ứng dụng.

ILoggerFactory

ILoggerFactory chịu trách nhiệm tạo các thể hiện ILogger.
Nó thường được dùng khi muốn tạo logger cho nhiều danh mục (category) khác nhau hoặc khi cần cấu hình riêng cho từng nhóm log.

Ví dụ:

public class MyService
{
    private readonly ILogger _logger;

    public MyService(ILoggerFactory loggerFactory)
    {
        _logger = loggerFactory.CreateLogger<MyService>();
    }

    public void DoWork()
    {
        _logger.LogInformation("Starting work.");
        try
        {
            // Thực thi công việc
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "An error occurred while doing work.");
        }
        _logger.LogInformation("Finished work.");
    }
}

Khác biệt chính giữa ILoggerILoggerFactory là:

  • ILogger được tiêm trực tiếp cho một class cụ thể.
  • ILoggerFactory cho phép tạo logger động theo danh mục, hữu ích khi ứng dụng có nhiều module.

ILoggerFactory cũng hỗ trợ cấu hình các provider (nhà cung cấp log) — ví dụ ghi log ra console, debug window, file, hoặc dịch vụ từ xa như Azure Application Insights, Elasticsearch,…


Ví dụ cấu hình logging trong Program.cs

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();

// Cấu hình logging
builder.Logging.ClearProviders();   // Xóa provider mặc định
builder.Logging.AddConsole();       // Ghi log ra console
builder.Logging.AddDebug();         // Ghi log cho debug window
builder.Logging.AddEventSourceLogger(); // Ghi log qua event source

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
}
else
{
    app.UseExceptionHandler("/Home/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.MapControllers();
app.Run();

Khi gọi _logger.LogInformation() hoặc _logger.LogError(), log sẽ được gửi tới tất cả các provider đã cấu hình.

Logging là công cụ mạnh mẽ và không thể thiếu — giúp phát hiện lỗi, phân tích hiệu năng, và tối ưu hệ thống.


Tóm tắt bài

Trong bài này, chúng ta đã học cách triển khai các thực hành tốt trong ASP.NET Core 9, bao gồm:

  • Làm việc đúng với các yêu cầu HTTP.
  • Thêm cachenén phản hồi để cải thiện hiệu năng.
  • Hiểu và áp dụng lập trình bất đồng bộ (async/await).
  • Tích hợp Redis để triển khai cache phân tán.
  • Thêm cơ chế phục hồi (retry, circuit breaker) bằng thư viện Polly.
  • Sử dụng logging và monitoring để theo dõi và duy trì ứng dụng.

Những kỹ thuật này giúp ứng dụng ASP.NET Core 9 trở nên hiệu quả, ổn định và đáng tin cậy hơn, đáp ứng tốt nhu cầu của hệ thống hiện đại.

Để lại một bình luận