Chuyển tới nội dung chính

10 bài viết được gắn thẻ "ASP.NET Core"

Pipeline, middleware, dependency injection và cách dựng Web API chịu được tải thật.

Xem tất cả thẻ

MailKit trong .NET: gửi email SMTP và dựng HTML email template chạy đúng trên Outlook

· 14 phút để đọc
Nguyễn Huỳnh Minh Tiến
Fullstack Developer @ Utop.io
Tóm tắt

MailKit là thư viện mail mã nguồn mở cho .NET (giấy phép MIT, tác giả Jeffrey Stedfast), nói được cả SMTP, IMAP và POP3. Tài liệu chính thức của Microsoft về System.Net.Mail.SmtpClient khuyến nghị thẳng việc dùng MailKit thay thế. Phần khó của việc gửi email không nằm ở chỗ gọi SendAsync — nó nằm ở cái HTML bên trong: Outlook classic render bằng engine của Word nên không có flex hay grid, Gmail cắt thư dài quá 102KB, và một thư chỉ có HTML mà thiếu bản plain-text thì điểm spam tăng ngay.

Gửi được email và gửi được email hiển thị đúng là hai bài toán khác nhau. Bài này giải quyết cả hai: phần đầu là cơ chế MailKit, phần sau là những ràng buộc rất cũ kỹ của HTML email mà không đọc trước thì sẽ mất buổi chiều ngồi hỏi vì sao cái template đẹp trên Chrome lại vỡ tan trên Outlook.

Một dấu tiếng Việt làm chết lời gọi API: cf-ipcity, HttpClient và giới hạn ASCII

· 12 phút để đọc
Nguyễn Huỳnh Minh Tiến
Fullstack Developer @ Utop.io
Tóm tắt

Cloudflare tự chèn cf-ipcity vào request đi vào, và với khách ở Việt Nam thì giá trị là Hồ Chí Minh — có dấu, tức non-ASCII. Service .NET forward nguyên xi mọi header sang lời gọi đi ra, nên giá trị đó rơi vào HttpClient. Điều bất ngờ là Headers.Add không ném exception, TryAddWithoutValidation cũng trả về true; mọi thứ chỉ nổ ở SendAsync với HttpRequestException: Request headers must contain only ASCII characters, và không byte nào rời khỏi tiến trình. Lỗi không nằm ở Cloudflare cũng không nằm ở .NET, mà ở chỗ ứng dụng forward mọi header vô điều kiện.

Bối cảnh là một nền tảng loyalty thương mại điện tử, quy mô khoảng ba triệu khách hàng. Tôi ẩn tên khách và mọi chi tiết định danh; phần kể được là phần kỹ thuật.

Mọi thứ nhìn đều bình thường. API đang chạy. Pod trên Kubernetes healthy. Database ổn. Request vào tới ứng dụng. Nhưng một lời gọi HTTP sang service nội bộ cứ hỏng trên production — và chỉ trên production.

Thứ làm nó hỏng, hoá ra, là tên thành phố của chính người dùng.

Forward header trong ASP.NET Core: vì sao 'forward hết' là một lỗi kiến trúc

· 13 phút để đọc
Nguyễn Huỳnh Minh Tiến
Fullstack Developer @ Utop.io
Tóm tắt

Vòng lặp copy mọi header từ request đi vào sang request đi ra là một anti-pattern, và nó hỏng theo ba hướng khác nhau: sai về tính đúng đắn (một giá trị non-ASCII làm HttpClient ném exception), sai về bảo mật (bạn tin và chuyển tiếp dữ liệu do client tự đặt), và sai về kiến trúc (header do hạ tầng chèn trở thành một phần hợp đồng của ứng dụng). Cách sửa là allowlist — khai tường minh danh sách header được phép đi qua — đặt trong một DelegatingHandler dùng chung. Khi viết handler đó, cẩn thận một cái bẫy: nó không sống trong scope của request.

Bài này khép lại cụm ba bài bắt đầu từ một sự cố production trên nền tảng loyalty khoảng ba triệu khách hàng, nơi header cf-ipcity: Hồ Chí Minh do Cloudflare chèn làm hỏng một lời gọi nội bộ. Bài đầu kể sự cố, bài hai giải thích vì sao header không mang được tiếng Việt. Bài này trả lời câu hỏi còn lại: viết lại chỗ đó thế nào cho đúng.

Unit of Work trong .NET và ABP: SaveChangesAsync không phải commit

· 12 phút để đọc
Nguyễn Huỳnh Minh Tiến
Fullstack Developer @ Utop.io
Tóm tắt

DbContext của EF Core đã là một Unit of Work sẵn: nó gom thay đổi trong change tracker rồi đẩy xuống database trong một transaction khi gọi SaveChanges. Vì vậy tự viết thêm một IUnitOfWork chỉ để gọi SaveChanges thường là thừa. ABP thì khác hẳn: Unit of Work ở đây là ambient, tự mở theo mỗi request, và SaveChangesAsync bên trong một UoW không commit transaction — chỉ CompleteAsync mới commit. Hiểu sai chỗ này là nguồn gốc của phần lớn bug "dữ liệu lúc có lúc không".

Unit of Work là một trong những pattern bị viết lại nhiều nhất trong thế giới .NET, và cũng là pattern bị viết lại một cách thừa thãi nhiều nhất. Bài này tách làm hai phần: EF Core thuần thì bạn cần gì, và ABP đã làm sẵn những gì mà bạn nên hiểu trước khi đụng vào.

Gọi .Result khi nào thì deadlock, khi nào thì không?

· 14 phút để đọc
Nguyễn Huỳnh Minh Tiến
Fullstack Developer @ Utop.io
Tóm tắt

.Result và .Wait() block thread hiện tại. Khi await tạm dừng, nó bắt lại SynchronizationContext đang hiện hành để chạy phần code phía sau đúng trên context đó. Nếu context chỉ cho phép một thread tại một thời điểm — UI thread của WPF, request context của ASP.NET Framework — mà thread đó lại đang bị .Result block, thì continuation không bao giờ vào được: deadlock. Console và ASP.NET Core không có SynchronizationContext, nên continuation chạy trên thread pool và không kẹt. Cách sửa thật sự là async all the way.

Đoạn code dưới đây chạy hoàn hảo trong một console app, và treo cứng khi bạn copy nguyên xi vào một controller của ASP.NET Framework hoặc một nút bấm trong WPF:

public string GetCustomerName(int id)
{
return GetCustomerAsync(id).Result; // treo ở đây
}

private async Task<string> GetCustomerAsync(int id)
{
var response = await _http.GetStringAsync($"/customers/{id}");
return Parse(response).Name;
}

Không có exception, không có stack trace, không có timeout. Thread đứng im vĩnh viễn. Chuyện này không phải do HttpClient, cũng không phải do async "bị lỗi" — nó là hệ quả trực tiếp của một quyết định thiết kế trong cách await hoạt động.