Các ứng dụng quản lý chấm công (timesheet) hiện đại đòi hỏi hệ thống thông báo email tin cậy để xử lý các tác vụ phê duyệt, gửi báo cáo và cập nhật trạng thái. Việc gửi email đồng bộ truyền thống có thể làm chậm ứng dụng và tạo ra trải nghiệm người dùng kém. Bài viết này sẽ hướng dẫn cách triển khai hệ thống thông báo email theo kiến trúc hướng sự kiện (event-driven) sử dụng Apache Kafka trong ASP.NET Core, đảm bảo khả năng mở rộng, độ tin cậy và sự tách biệt rõ ràng giữa các thành phần.
Tại sao nên sử dụng Email Notification hướng sự kiện?
Khi người dùng nhấn "Gửi timesheet", họ mong muốn nhận được phản hồi tức thì. Tuy nhiên, việc kết nối và gửi email đồng bộ có thể mất vài giây, gây nghẽn luồng xử lý (request thread) và giảm hiệu năng. Kiến trúc hướng sự kiện giải quyết vấn đề này thông qua:
- Giảm sự phụ thuộc (Decoupling): Việc gửi timesheet và gửi email trở nên độc lập với nhau.
- Cải thiện thời gian phản hồi: Người dùng nhận được phản hồi ngay lập tức trong khi email được xử lý ngầm.
- Đảm bảo độ tin cậy: Các nỗ lực gửi email thất bại không làm treo ứng dụng chính.
- Khả năng mở rộng: Xử lý email có thể mở rộng độc lập với logic nghiệp vụ cốt lõi.
Tổng quan về Kiến trúc
Trong một môi trường doanh nghiệp chuyên nghiệp, chúng ta tránh việc gắn chặt logic nghiệp vụ với phương thức truyền tải thông báo. Hệ thống của chúng ta triển khai luồng thông báo ba lớp được thiết kế cho tính sẵn sàng cao.
1. Lớp Sự kiện Miền (Domain Event Layer)
Khi các sự kiện nghiệp vụ xảy ra (gửi, phê duyệt hoặc từ chối timesheet), ứng dụng sẽ phát đi các "Domain Event" sử dụng pattern Notification của MediatR. Những sự kiện này ghi lại những gì đã xảy ra mà không cần quan tâm đến việc xử lý chúng như thế nào.
// Sự kiện miền được phát đi sau khi gửi timesheet
public class TimesheetSubmittedEvent : INotification
{
public Guid TimesheetId { get; set; }
public Guid SubmitterId { get; set; }
public Guid ApproverId { get; set; }
public DateTime SubmittedAt { get; set; }
}
2. Lớp Xử lý Sự kiện (Event Handler Layer)
Các trình xử lý sự kiện (Event Handlers) lắng nghe các sự kiện miền và chuyển đổi chúng thành thông điệp Kafka. Lớp này xác định ai sẽ nhận email, tạo bản ghi thông báo trong cơ sở dữ liệu để kiểm toán và chuẩn bị dữ liệu cụ thể cho hàng đợi thông điệp.
// Handler chuyển đổi event thành message Kafka
public class TimesheetSubmittedHandler : INotificationHandler<TimesheetSubmittedEvent>
{
public async Task Handle(TimesheetSubmittedEvent notification, CancellationToken ct)
{
var mailMessage = new MailMessageV1
{
To = GetApproverEmail(notification.ApproverId),
Subject = "Timesheet mới đang chờ phê duyệt",
Body = $"Timesheet {notification.TimesheetId} đã được gửi lúc {notification.SubmittedAt}"
};
// Thêm vào bộ sưu tập message để Pipeline xử lý sau
_kafkaMessages.MailMessages.Add(new KafkaMessage<MailMessageV1>
{
NotificationId = Guid.NewGuid(),
Value = mailMessage
});
}
}
3. Lớp Kafka Pipeline (Kafka Pipeline Layer)
KafkaPipeline đóng vai trò là một bộ hậu xử lý chạy sau khi request chính hoàn thành thành công. Điều này đảm bảo chúng ta chỉ gửi thông báo nếu giao dịch cơ sở dữ liệu của timesheet đã được commit. Nó đẩy tất cả các thông điệp đã xếp hàng vào các topic của Kafka.
// Pipeline đẩy message vào Kafka sau khi hoàn tất request
public class KafkaPipeline<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
{
public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken ct)
{
var response = await next(); // Thực thi xử lý nghiệp vụ chính trước
try
{
foreach (var message in _kafkaMessages.MailMessages)
{
await _producers.MailProducer.ProduceRetryAsync(
AppSettings.Kafka.MailTopic,
message
);
// Đánh dấu thông báo đã vào hàng đợi trong DB
await _notificationRepo.MarkAsQueued(message.NotificationId);
}
}
catch (Exception e)
{
_logger.LogError(e, "Lỗi khi gửi dữ liệu vào hàng đợi Kafka");
}
return response;
}
}
Phân tích Chiến lược: Độ tin cậy và Giám sát
Sử dụng Kafka mang lại nhiều lợi ích hơn là chỉ tốc độ; nó cung cấp một "vùng đệm" cho hệ thống. Nếu nhà cung cấp dịch vụ SMTP của bạn (như SendGrid hoặc MailGun) gặp sự cố, các thông điệp vẫn nằm an toàn trong Kafka topic. Khi dịch vụ Mail Consumer được khôi phục, nó sẽ tiếp tục xử lý chính xác từ vị trí bị dừng lại, đảm bảo không có email "Yêu cầu phê duyệt" nào bị thất lạc.
Lợi ích Chính
- Xử lý bất đồng bộ: Người dùng không phải chờ đợi quá trình bắt tay SMTP.
- Khả năng chịu lỗi: Nếu Kafka tạm thời không khả dụng, ứng dụng chính vẫn hoạt động bình thường.
- Nhật ký kiểm toán (Audit Trail): Mọi nỗ lực thông báo đều được ghi lại với cờ
IsSentToQueue, giúp minh bạch trạng thái gửi. - An toàn môi trường: Các môi trường phát triển (Dev) có thể bỏ qua cấu hình Kafka mà không làm hỏng logic hệ thống.
Các quy tắc tốt nhất (Best Practices)
- Sử dụng Topic riêng biệt: Phân chia các topic Kafka cho các loại thông báo khác nhau (Mail, Push, SMS).
- Triển khai cơ chế thử lại (Retry): Sử dụng exponential backoff cho các lỗi kết nối Kafka tạm thời.
- Giám sát độ trễ hàng đợi: Thiết lập cảnh báo nếu độ trễ (lag) của
mail-topicvượt quá ngưỡng cho phép. - Phiên bản hóa thông điệp: Sử dụng schema registry hoặc các lớp phiên bản (V1, V2) để quản lý thay đổi trong tương lai.
Kết luận
Thông báo email hướng sự kiện với Kafka giúp chuyển đổi ứng dụng timesheet từ các hoạt động gây nghẽn thành các hệ thống phản hồi nhanh, đẳng cấp doanh nghiệp. Bằng cách tách biệt logic nghiệp vụ khỏi các giao thức truyền thông, bạn có được một kiến trúc dễ bảo trì và có khả năng mở rộng mạnh mẽ. Đây là một mô hình thiết yếu cho bất kỳ ứng dụng ASP.NET Core hiệu năng cao nào yêu cầu xử lý nền tảng vững chắc.