Migrate
This commit is contained in:
349
microservices/.agent/skills/api-design/SKILL.md
Normal file
349
microservices/.agent/skills/api-design/SKILL.md
Normal file
@@ -0,0 +1,349 @@
|
||||
---
|
||||
name: api-design
|
||||
description: RESTful API design standards for GoodGo microservices. Use for new API endpoints, DTOs, controllers, OpenAPI documentation, or standardized responses.
|
||||
compatibility: ".NET 10+, ASP.NET Core, MediatR, Swashbuckle, Asp.Versioning"
|
||||
metadata:
|
||||
author: Velik Ho
|
||||
version: "2.0"
|
||||
---
|
||||
|
||||
# RESTful API Design Standards / Tiêu Chuẩn Thiết Kế RESTful API
|
||||
|
||||
Standards for designing consistent, maintainable APIs in GoodGo microservices.
|
||||
|
||||
## When to Use This Skill / Khi Nào Sử Dụng
|
||||
|
||||
Use this skill when:
|
||||
- Creating new API endpoints / Tạo API endpoints mới
|
||||
- Designing request/response DTOs / Thiết kế DTOs
|
||||
- Implementing controllers with MediatR / Triển khai controllers với MediatR
|
||||
- Writing OpenAPI/Swagger documentation / Viết tài liệu OpenAPI/Swagger
|
||||
- Standardizing error responses / Chuẩn hóa error responses
|
||||
- Implementing pagination, filtering / Triển khai pagination, filtering
|
||||
|
||||
## Core Concepts / Khái Niệm Cốt Lõi
|
||||
|
||||
### Clean Architecture Layers / Các Tầng Clean Architecture
|
||||
|
||||
```
|
||||
src/
|
||||
├── ServiceName.API/ # Controllers, DTOs, Middleware
|
||||
│ ├── Controllers/ # API Controllers
|
||||
│ └── Application/ # Commands, Queries, Handlers
|
||||
├── ServiceName.Domain/ # Entities, Aggregates, Interfaces
|
||||
└── ServiceName.Infrastructure/ # Repositories, External Services
|
||||
```
|
||||
|
||||
### API Response Wrapper / Wrapper Response Chuẩn
|
||||
|
||||
```csharp
|
||||
/// <summary>
|
||||
/// EN: Standard API response wrapper.
|
||||
/// VI: Wrapper response API chuẩn.
|
||||
/// </summary>
|
||||
public class ApiResponse<T>
|
||||
{
|
||||
public bool Success { get; set; }
|
||||
public T? Data { get; set; }
|
||||
public string? Error { get; set; }
|
||||
public PaginationInfo? Pagination { get; set; }
|
||||
}
|
||||
|
||||
public record PaginationInfo(
|
||||
int Page,
|
||||
int Limit,
|
||||
int Total,
|
||||
int TotalPages);
|
||||
```
|
||||
|
||||
### URL Structure / Cấu Trúc URL
|
||||
|
||||
```
|
||||
/api/v{version}/{resource}/{id?}/{sub-resource?}
|
||||
|
||||
GET /api/v1/files # List files
|
||||
POST /api/v1/files # Create file
|
||||
GET /api/v1/files/{id} # Get file by ID
|
||||
PUT /api/v1/files/{id} # Update file
|
||||
DELETE /api/v1/files/{id} # Delete file
|
||||
GET /api/v1/files/{id}/versions # Get file versions
|
||||
```
|
||||
|
||||
## Key Patterns / Mẫu Chính
|
||||
|
||||
### Controller with MediatR / Controller với MediatR
|
||||
|
||||
```csharp
|
||||
/// <summary>
|
||||
/// EN: Controller for file operations.
|
||||
/// VI: Controller cho các thao tác file.
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[ApiVersion("1.0")]
|
||||
[Route("api/v{version:apiVersion}/files")]
|
||||
[SwaggerTag("File Management - Upload, download, and manage files")]
|
||||
public class FilesController : ControllerBase
|
||||
{
|
||||
private readonly IMediator _mediator;
|
||||
|
||||
public FilesController(IMediator mediator)
|
||||
{
|
||||
_mediator = mediator;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Get file by ID.
|
||||
/// VI: Lấy thông tin file theo ID.
|
||||
/// </summary>
|
||||
[HttpGet("{fileId:guid}")]
|
||||
[Authorize]
|
||||
[SwaggerOperation(Summary = "Get file by ID")]
|
||||
[SwaggerResponse(200, "File retrieved successfully")]
|
||||
[SwaggerResponse(404, "File not found")]
|
||||
public async Task<ActionResult<ApiResponse<FileDto>>> GetFile(
|
||||
Guid fileId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var userId = GetUserId();
|
||||
if (string.IsNullOrEmpty(userId))
|
||||
return Unauthorized(new ApiResponse<FileDto>
|
||||
{
|
||||
Success = false,
|
||||
Error = "User ID not found"
|
||||
});
|
||||
|
||||
var query = new GetFileQuery(fileId, userId);
|
||||
var result = await _mediator.Send(query, cancellationToken);
|
||||
|
||||
if (result == null)
|
||||
return NotFound(new ApiResponse<FileDto>
|
||||
{
|
||||
Success = false,
|
||||
Error = "File not found"
|
||||
});
|
||||
|
||||
return Ok(new ApiResponse<FileDto> { Success = true, Data = result });
|
||||
}
|
||||
|
||||
private string? GetUserId() =>
|
||||
User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||
}
|
||||
```
|
||||
|
||||
### DTO with Records / DTO với Records
|
||||
|
||||
```csharp
|
||||
/// <summary>
|
||||
/// EN: DTO for file information.
|
||||
/// VI: DTO cho thông tin file.
|
||||
/// </summary>
|
||||
public record FileDto(
|
||||
Guid Id,
|
||||
string UserId,
|
||||
string FileName,
|
||||
string ContentType,
|
||||
long FileSizeBytes,
|
||||
string AccessLevel,
|
||||
DateTime UploadedAt);
|
||||
|
||||
/// <summary>
|
||||
/// EN: Result for user files query with pagination.
|
||||
/// VI: Kết quả query files với phân trang.
|
||||
/// </summary>
|
||||
public record UserFilesResult(
|
||||
IReadOnlyList<FileDto> Files,
|
||||
int TotalCount);
|
||||
|
||||
/// <summary>
|
||||
/// EN: Mapper from domain entities to DTOs.
|
||||
/// VI: Mapper từ domain entities sang DTOs.
|
||||
/// </summary>
|
||||
public static class FileDtoMapper
|
||||
{
|
||||
public static FileDto ToDto(this StorageFile file) => new(
|
||||
file.Id,
|
||||
file.UserId,
|
||||
file.FileName,
|
||||
file.ContentType,
|
||||
file.FileSizeBytes,
|
||||
file.AccessLevel.ToString(),
|
||||
file.UploadedAt);
|
||||
}
|
||||
```
|
||||
|
||||
### Request Validation / Validation Request
|
||||
|
||||
```csharp
|
||||
/// <summary>
|
||||
/// EN: Command for file upload.
|
||||
/// VI: Command để upload file.
|
||||
/// </summary>
|
||||
public record UploadFileCommand(
|
||||
Stream FileStream,
|
||||
string FileName,
|
||||
string ContentType,
|
||||
long FileSize,
|
||||
string UserId,
|
||||
Guid? FolderId,
|
||||
FileAccessLevel AccessLevel) : IRequest<UploadFileResult>;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Query for user files with pagination.
|
||||
/// VI: Query lấy files với phân trang.
|
||||
/// </summary>
|
||||
public record GetUserFilesQuery(
|
||||
string UserId,
|
||||
int Skip = 0,
|
||||
int Take = 20,
|
||||
string? Search = null) : IRequest<UserFilesResult>;
|
||||
```
|
||||
|
||||
### List Endpoint with Pagination / Endpoint List với Phân Trang
|
||||
|
||||
```csharp
|
||||
[HttpGet]
|
||||
[Authorize]
|
||||
[SwaggerOperation(Summary = "Get user files")]
|
||||
public async Task<ActionResult<ApiResponse<UserFilesResult>>> GetFiles(
|
||||
[FromQuery] int skip = 0,
|
||||
[FromQuery] int take = 20,
|
||||
[FromQuery] string? search = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var userId = GetUserId();
|
||||
if (string.IsNullOrEmpty(userId))
|
||||
return Unauthorized(new ApiResponse<UserFilesResult>
|
||||
{
|
||||
Success = false,
|
||||
Error = "User ID not found"
|
||||
});
|
||||
|
||||
var query = new GetUserFilesQuery(userId, skip, take, search);
|
||||
var result = await _mediator.Send(query, cancellationToken);
|
||||
|
||||
return Ok(new ApiResponse<UserFilesResult>
|
||||
{
|
||||
Success = true,
|
||||
Data = result,
|
||||
Pagination = new PaginationInfo(
|
||||
Page: skip / take + 1,
|
||||
Limit: take,
|
||||
Total: result.TotalCount,
|
||||
TotalPages: (int)Math.Ceiling(result.TotalCount / (double)take))
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## Common Mistakes / Lỗi Thường Gặp
|
||||
|
||||
### 1. Inconsistent Response Format / Response Format Không Nhất Quán
|
||||
|
||||
```csharp
|
||||
// ❌ BAD: Trả về trực tiếp data
|
||||
return Ok(result);
|
||||
|
||||
// ✅ GOOD: Dùng ApiResponse wrapper
|
||||
return Ok(new ApiResponse<FileDto> { Success = true, Data = result });
|
||||
```
|
||||
|
||||
### 2. Missing API Versioning / Thiếu API Versioning
|
||||
|
||||
```csharp
|
||||
// ❌ BAD: Không có versioning
|
||||
[Route("api/files")]
|
||||
|
||||
// ✅ GOOD: Có API versioning
|
||||
[ApiVersion("1.0")]
|
||||
[Route("api/v{version:apiVersion}/files")]
|
||||
```
|
||||
|
||||
### 3. No Bilingual XML Comments / Thiếu Comment Song Ngữ
|
||||
|
||||
```csharp
|
||||
// ❌ BAD: Chỉ tiếng Anh
|
||||
/// <summary>Get file by ID.</summary>
|
||||
|
||||
// ✅ GOOD: Song ngữ EN/VI
|
||||
/// <summary>
|
||||
/// EN: Get file by ID.
|
||||
/// VI: Lấy thông tin file theo ID.
|
||||
/// </summary>
|
||||
```
|
||||
|
||||
### 4. Missing Swagger Annotations / Thiếu Swagger Annotations
|
||||
|
||||
```csharp
|
||||
// ❌ BAD: Không có Swagger annotations
|
||||
[HttpGet("{fileId:guid}")]
|
||||
public async Task<ActionResult<FileDto>> GetFile(Guid fileId)
|
||||
|
||||
// ✅ GOOD: Đầy đủ Swagger annotations
|
||||
[HttpGet("{fileId:guid}")]
|
||||
[SwaggerOperation(Summary = "Get file by ID")]
|
||||
[SwaggerResponse(200, "File retrieved successfully")]
|
||||
[SwaggerResponse(404, "File not found")]
|
||||
public async Task<ActionResult<ApiResponse<FileDto>>> GetFile(Guid fileId)
|
||||
```
|
||||
|
||||
### 5. Returning 200 for Errors / Trả 200 Cho Lỗi
|
||||
|
||||
```csharp
|
||||
// ❌ BAD: 200 cho error
|
||||
return Ok(new ApiResponse<FileDto> { Success = false, Error = "Not found" });
|
||||
|
||||
// ✅ GOOD: Đúng status code
|
||||
return NotFound(new ApiResponse<FileDto> { Success = false, Error = "File not found" });
|
||||
```
|
||||
|
||||
## Quick Reference / Tham Chiếu Nhanh
|
||||
|
||||
### HTTP Methods & Status Codes
|
||||
|
||||
| Method | Action | Success | Error Codes |
|
||||
|--------|--------|---------|-------------|
|
||||
| **GET** | Retrieve | 200 | 404 |
|
||||
| **POST** | Create | 200/201 | 400, 409 |
|
||||
| **PUT** | Full update | 200 | 400, 404 |
|
||||
| **PATCH** | Partial update | 200 | 400, 404 |
|
||||
| **DELETE** | Remove | 200/204 | 404 |
|
||||
|
||||
### Common Error Codes
|
||||
|
||||
| Code | Meaning | When to Use |
|
||||
|------|---------|-------------|
|
||||
| 400 | Bad Request | Validation errors |
|
||||
| 401 | Unauthorized | Missing/invalid token |
|
||||
| 403 | Forbidden | No permission |
|
||||
| 404 | Not Found | Resource doesn't exist |
|
||||
| 409 | Conflict | Duplicate resource |
|
||||
| 422 | Unprocessable | Business rule violation |
|
||||
| 429 | Too Many Requests | Rate limited |
|
||||
|
||||
### Controller Attributes
|
||||
|
||||
| Attribute | Purpose |
|
||||
|-----------|---------|
|
||||
| `[ApiController]` | Enable API behaviors |
|
||||
| `[ApiVersion("1.0")]` | API versioning |
|
||||
| `[Authorize]` | Require authentication |
|
||||
| `[SwaggerTag("Description")]` | Swagger grouping |
|
||||
| `[SwaggerOperation]` | Endpoint documentation |
|
||||
| `[SwaggerResponse]` | Response documentation |
|
||||
|
||||
### ApiResponse Format
|
||||
|
||||
```csharp
|
||||
// Success response
|
||||
{ "success": true, "data": {...}, "pagination": {...} }
|
||||
|
||||
// Error response
|
||||
{ "success": false, "error": "Error message" }
|
||||
```
|
||||
|
||||
## Resources / Tài Nguyên
|
||||
|
||||
- [API Versioning Strategy](../api-versioning-strategy/SKILL.md) - Versioning patterns
|
||||
- [Error Handling Patterns](../error-handling-patterns/SKILL.md) - Error handling
|
||||
- [Middleware Patterns](../middleware-patterns/SKILL.md) - Request handling
|
||||
- [Project Rules](../project-rules/SKILL.md) - Coding standards
|
||||
- [Skill Authoring](../skill-authoring/SKILL.md) - How to write skills
|
||||
575
microservices/.agent/skills/api-design/references/REFERENCE.md
Normal file
575
microservices/.agent/skills/api-design/references/REFERENCE.md
Normal file
@@ -0,0 +1,575 @@
|
||||
# API Design - Detailed Reference
|
||||
|
||||
This reference contains detailed code examples for RESTful API design patterns in ASP.NET Core.
|
||||
|
||||
## Standard Response Format / Định Dạng Response Chuẩn
|
||||
|
||||
### ApiResponse Wrapper
|
||||
|
||||
```csharp
|
||||
/// <summary>
|
||||
/// EN: Standard API response wrapper.
|
||||
/// VI: Wrapper response API chuẩn.
|
||||
/// </summary>
|
||||
public class ApiResponse<T>
|
||||
{
|
||||
public bool Success { get; set; }
|
||||
public T? Data { get; set; }
|
||||
public string? Error { get; set; }
|
||||
public PaginationInfo? Pagination { get; set; }
|
||||
public MetadataInfo? Metadata { get; set; }
|
||||
}
|
||||
|
||||
public record PaginationInfo(
|
||||
int Page,
|
||||
int Limit,
|
||||
int Total,
|
||||
int TotalPages);
|
||||
|
||||
public record MetadataInfo(
|
||||
string Timestamp,
|
||||
string Version,
|
||||
string RequestId);
|
||||
|
||||
// EN: Success response example / VI: Ví dụ response thành công
|
||||
// {
|
||||
// "success": true,
|
||||
// "data": { "id": "123", "email": "user@example.com" },
|
||||
// "pagination": { "page": 1, "limit": 10, "total": 100, "totalPages": 10 }
|
||||
// }
|
||||
|
||||
// EN: Error response example / VI: Ví dụ response lỗi
|
||||
// {
|
||||
// "success": false,
|
||||
// "error": "User not found"
|
||||
// }
|
||||
```
|
||||
|
||||
## DTOs (Data Transfer Objects)
|
||||
|
||||
### Request DTOs with Records
|
||||
|
||||
```csharp
|
||||
/// <summary>
|
||||
/// EN: DTO for creating a user.
|
||||
/// VI: DTO để tạo user.
|
||||
/// </summary>
|
||||
public record CreateUserRequest(
|
||||
[Required]
|
||||
[EmailAddress]
|
||||
string Email,
|
||||
|
||||
[Required]
|
||||
[MinLength(6)]
|
||||
string Password,
|
||||
|
||||
string? Name);
|
||||
|
||||
/// <summary>
|
||||
/// EN: DTO for updating a user.
|
||||
/// VI: DTO để cập nhật user.
|
||||
/// </summary>
|
||||
public record UpdateUserRequest(
|
||||
[EmailAddress]
|
||||
string? Email,
|
||||
|
||||
string? Name,
|
||||
|
||||
string? Avatar);
|
||||
|
||||
/// <summary>
|
||||
/// EN: Query parameters for listing users.
|
||||
/// VI: Query parameters để list users.
|
||||
/// </summary>
|
||||
public record GetUsersQuery(
|
||||
int Skip = 0,
|
||||
int Take = 20,
|
||||
string? Search = null,
|
||||
string SortBy = "CreatedAt",
|
||||
string Order = "desc");
|
||||
```
|
||||
|
||||
### Response DTOs
|
||||
|
||||
```csharp
|
||||
/// <summary>
|
||||
/// EN: DTO for user information.
|
||||
/// VI: DTO cho thông tin user.
|
||||
/// </summary>
|
||||
public record UserDto(
|
||||
Guid Id,
|
||||
string Email,
|
||||
string? Name,
|
||||
string? Avatar,
|
||||
string Role,
|
||||
DateTime CreatedAt,
|
||||
DateTime UpdatedAt);
|
||||
|
||||
/// <summary>
|
||||
/// EN: Result for paginated user list.
|
||||
/// VI: Kết quả danh sách user có phân trang.
|
||||
/// </summary>
|
||||
public record UsersListResult(
|
||||
IReadOnlyList<UserDto> Users,
|
||||
int TotalCount);
|
||||
|
||||
/// <summary>
|
||||
/// EN: Mapper from domain entities to DTOs.
|
||||
/// VI: Mapper từ domain entities sang DTOs.
|
||||
/// </summary>
|
||||
public static class UserDtoMapper
|
||||
{
|
||||
public static UserDto ToDto(this User user) => new(
|
||||
user.Id,
|
||||
user.Email,
|
||||
user.Name,
|
||||
user.Avatar,
|
||||
user.Role.ToString(),
|
||||
user.CreatedAt,
|
||||
user.UpdatedAt);
|
||||
}
|
||||
```
|
||||
|
||||
## Controller Implementation / Triển Khai Controller
|
||||
|
||||
### Complete Controller Example
|
||||
|
||||
```csharp
|
||||
/// <summary>
|
||||
/// EN: Controller for user management.
|
||||
/// VI: Controller quản lý user.
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[ApiVersion("1.0")]
|
||||
[Route("api/v{version:apiVersion}/users")]
|
||||
[SwaggerTag("User Management - Create, read, update, and delete users")]
|
||||
public class UsersController : ControllerBase
|
||||
{
|
||||
private readonly IMediator _mediator;
|
||||
private readonly ILogger<UsersController> _logger;
|
||||
|
||||
public UsersController(IMediator mediator, ILogger<UsersController> logger)
|
||||
{
|
||||
_mediator = mediator;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Get list of users with pagination.
|
||||
/// VI: Lấy danh sách users với phân trang.
|
||||
/// </summary>
|
||||
[HttpGet]
|
||||
[Authorize(Roles = "Admin")]
|
||||
[SwaggerOperation(Summary = "List users", Description = "Get paginated list of users")]
|
||||
[SwaggerResponse(200, "Users retrieved successfully")]
|
||||
[SwaggerResponse(401, "Unauthorized")]
|
||||
[SwaggerResponse(403, "Forbidden - Admin role required")]
|
||||
public async Task<ActionResult<ApiResponse<UsersListResult>>> GetUsers(
|
||||
[FromQuery] int skip = 0,
|
||||
[FromQuery] int take = 20,
|
||||
[FromQuery] string? search = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var query = new GetUsersQuery(skip, take, search);
|
||||
var result = await _mediator.Send(query, cancellationToken);
|
||||
|
||||
return Ok(new ApiResponse<UsersListResult>
|
||||
{
|
||||
Success = true,
|
||||
Data = result,
|
||||
Pagination = new PaginationInfo(
|
||||
Page: skip / take + 1,
|
||||
Limit: take,
|
||||
Total: result.TotalCount,
|
||||
TotalPages: (int)Math.Ceiling(result.TotalCount / (double)take))
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Get user by ID.
|
||||
/// VI: Lấy user theo ID.
|
||||
/// </summary>
|
||||
[HttpGet("{userId:guid}")]
|
||||
[Authorize]
|
||||
[SwaggerOperation(Summary = "Get user by ID")]
|
||||
[SwaggerResponse(200, "User retrieved successfully")]
|
||||
[SwaggerResponse(404, "User not found")]
|
||||
public async Task<ActionResult<ApiResponse<UserDto>>> GetUser(
|
||||
Guid userId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var query = new GetUserByIdQuery(userId);
|
||||
var result = await _mediator.Send(query, cancellationToken);
|
||||
|
||||
if (result == null)
|
||||
return NotFound(new ApiResponse<UserDto>
|
||||
{
|
||||
Success = false,
|
||||
Error = "User not found"
|
||||
});
|
||||
|
||||
return Ok(new ApiResponse<UserDto> { Success = true, Data = result });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Create a new user.
|
||||
/// VI: Tạo user mới.
|
||||
/// </summary>
|
||||
[HttpPost]
|
||||
[SwaggerOperation(Summary = "Create user")]
|
||||
[SwaggerResponse(201, "User created successfully")]
|
||||
[SwaggerResponse(400, "Invalid request")]
|
||||
[SwaggerResponse(409, "User already exists")]
|
||||
public async Task<ActionResult<ApiResponse<UserDto>>> CreateUser(
|
||||
[FromBody] CreateUserRequest request,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var command = new CreateUserCommand(request.Email, request.Password, request.Name);
|
||||
var result = await _mediator.Send(command, cancellationToken);
|
||||
|
||||
if (!result.Success)
|
||||
return BadRequest(new ApiResponse<UserDto>
|
||||
{
|
||||
Success = false,
|
||||
Error = result.Error
|
||||
});
|
||||
|
||||
return CreatedAtAction(
|
||||
nameof(GetUser),
|
||||
new { userId = result.User!.Id },
|
||||
new ApiResponse<UserDto> { Success = true, Data = result.User });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Update user by ID.
|
||||
/// VI: Cập nhật user theo ID.
|
||||
/// </summary>
|
||||
[HttpPut("{userId:guid}")]
|
||||
[Authorize]
|
||||
[SwaggerOperation(Summary = "Update user")]
|
||||
[SwaggerResponse(200, "User updated successfully")]
|
||||
[SwaggerResponse(400, "Invalid request")]
|
||||
[SwaggerResponse(404, "User not found")]
|
||||
public async Task<ActionResult<ApiResponse<UserDto>>> UpdateUser(
|
||||
Guid userId,
|
||||
[FromBody] UpdateUserRequest request,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
// EN: Check if user is updating their own profile or is admin
|
||||
// VI: Kiểm tra user có đang cập nhật profile của mình hoặc là admin
|
||||
var currentUserId = GetUserId();
|
||||
if (currentUserId != userId.ToString() && !User.IsInRole("Admin"))
|
||||
return Forbid();
|
||||
|
||||
var command = new UpdateUserCommand(userId, request.Email, request.Name, request.Avatar);
|
||||
var result = await _mediator.Send(command, cancellationToken);
|
||||
|
||||
if (!result.Success)
|
||||
return NotFound(new ApiResponse<UserDto>
|
||||
{
|
||||
Success = false,
|
||||
Error = result.Error
|
||||
});
|
||||
|
||||
return Ok(new ApiResponse<UserDto> { Success = true, Data = result.User });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Delete user by ID.
|
||||
/// VI: Xóa user theo ID.
|
||||
/// </summary>
|
||||
[HttpDelete("{userId:guid}")]
|
||||
[Authorize(Roles = "Admin")]
|
||||
[SwaggerOperation(Summary = "Delete user")]
|
||||
[SwaggerResponse(204, "User deleted successfully")]
|
||||
[SwaggerResponse(404, "User not found")]
|
||||
public async Task<ActionResult> DeleteUser(
|
||||
Guid userId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var command = new DeleteUserCommand(userId);
|
||||
var result = await _mediator.Send(command, cancellationToken);
|
||||
|
||||
if (!result.Success)
|
||||
return NotFound(new ApiResponse<object>
|
||||
{
|
||||
Success = false,
|
||||
Error = result.Error
|
||||
});
|
||||
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
private string? GetUserId() =>
|
||||
User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||
}
|
||||
```
|
||||
|
||||
## MediatR Commands & Queries / Commands & Queries MediatR
|
||||
|
||||
### Commands
|
||||
|
||||
```csharp
|
||||
/// <summary>
|
||||
/// EN: Command to create a user.
|
||||
/// VI: Command tạo user.
|
||||
/// </summary>
|
||||
public record CreateUserCommand(
|
||||
string Email,
|
||||
string Password,
|
||||
string? Name) : IRequest<CreateUserResult>;
|
||||
|
||||
public record CreateUserResult(
|
||||
bool Success,
|
||||
UserDto? User,
|
||||
string? Error);
|
||||
|
||||
/// <summary>
|
||||
/// EN: Handler for CreateUserCommand.
|
||||
/// VI: Handler cho CreateUserCommand.
|
||||
/// </summary>
|
||||
public class CreateUserCommandHandler : IRequestHandler<CreateUserCommand, CreateUserResult>
|
||||
{
|
||||
private readonly IUserRepository _userRepository;
|
||||
private readonly IPasswordHasher _passwordHasher;
|
||||
|
||||
public CreateUserCommandHandler(
|
||||
IUserRepository userRepository,
|
||||
IPasswordHasher passwordHasher)
|
||||
{
|
||||
_userRepository = userRepository;
|
||||
_passwordHasher = passwordHasher;
|
||||
}
|
||||
|
||||
public async Task<CreateUserResult> Handle(
|
||||
CreateUserCommand request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// EN: Check if user already exists
|
||||
// VI: Kiểm tra user đã tồn tại chưa
|
||||
var existingUser = await _userRepository.FindByEmailAsync(request.Email, cancellationToken);
|
||||
if (existingUser != null)
|
||||
return new CreateUserResult(false, null, "User with this email already exists");
|
||||
|
||||
// EN: Create new user
|
||||
// VI: Tạo user mới
|
||||
var user = new User(
|
||||
email: request.Email,
|
||||
passwordHash: _passwordHasher.Hash(request.Password),
|
||||
name: request.Name);
|
||||
|
||||
await _userRepository.AddAsync(user, cancellationToken);
|
||||
await _userRepository.SaveChangesAsync(cancellationToken);
|
||||
|
||||
return new CreateUserResult(true, user.ToDto(), null);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Queries
|
||||
|
||||
```csharp
|
||||
/// <summary>
|
||||
/// EN: Query to get users with pagination.
|
||||
/// VI: Query lấy users với phân trang.
|
||||
/// </summary>
|
||||
public record GetUsersQuery(
|
||||
int Skip = 0,
|
||||
int Take = 20,
|
||||
string? Search = null) : IRequest<UsersListResult>;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Handler for GetUsersQuery.
|
||||
/// VI: Handler cho GetUsersQuery.
|
||||
/// </summary>
|
||||
public class GetUsersQueryHandler : IRequestHandler<GetUsersQuery, UsersListResult>
|
||||
{
|
||||
private readonly IUserRepository _userRepository;
|
||||
|
||||
public GetUsersQueryHandler(IUserRepository userRepository)
|
||||
{
|
||||
_userRepository = userRepository;
|
||||
}
|
||||
|
||||
public async Task<UsersListResult> Handle(
|
||||
GetUsersQuery request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var (users, totalCount) = await _userRepository.GetUsersAsync(
|
||||
request.Skip,
|
||||
request.Take,
|
||||
request.Search,
|
||||
cancellationToken);
|
||||
|
||||
return new UsersListResult(
|
||||
users.Select(u => u.ToDto()).ToList(),
|
||||
totalCount);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Validation with FluentValidation / Validation với FluentValidation
|
||||
|
||||
```csharp
|
||||
/// <summary>
|
||||
/// EN: Validator for CreateUserRequest.
|
||||
/// VI: Validator cho CreateUserRequest.
|
||||
/// </summary>
|
||||
public class CreateUserRequestValidator : AbstractValidator<CreateUserRequest>
|
||||
{
|
||||
public CreateUserRequestValidator()
|
||||
{
|
||||
RuleFor(x => x.Email)
|
||||
.NotEmpty().WithMessage("Email is required")
|
||||
.EmailAddress().WithMessage("Invalid email format");
|
||||
|
||||
RuleFor(x => x.Password)
|
||||
.NotEmpty().WithMessage("Password is required")
|
||||
.MinimumLength(6).WithMessage("Password must be at least 6 characters")
|
||||
.Matches("[A-Z]").WithMessage("Password must contain at least one uppercase letter")
|
||||
.Matches("[0-9]").WithMessage("Password must contain at least one digit");
|
||||
|
||||
RuleFor(x => x.Name)
|
||||
.MaximumLength(100).WithMessage("Name cannot exceed 100 characters");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Error Handling / Xử Lý Lỗi
|
||||
|
||||
### Global Exception Handler Middleware
|
||||
|
||||
```csharp
|
||||
/// <summary>
|
||||
/// EN: Global exception handler middleware.
|
||||
/// VI: Middleware xử lý exception toàn cục.
|
||||
/// </summary>
|
||||
public class ExceptionHandlerMiddleware
|
||||
{
|
||||
private readonly RequestDelegate _next;
|
||||
private readonly ILogger<ExceptionHandlerMiddleware> _logger;
|
||||
|
||||
public ExceptionHandlerMiddleware(
|
||||
RequestDelegate next,
|
||||
ILogger<ExceptionHandlerMiddleware> logger)
|
||||
{
|
||||
_next = next;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task InvokeAsync(HttpContext context)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _next(context);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await HandleExceptionAsync(context, ex);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleExceptionAsync(HttpContext context, Exception exception)
|
||||
{
|
||||
_logger.LogError(exception, "Unhandled exception occurred");
|
||||
|
||||
var (statusCode, error) = exception switch
|
||||
{
|
||||
ValidationException ve => (400, ve.Message),
|
||||
UnauthorizedAccessException => (401, "Unauthorized"),
|
||||
KeyNotFoundException => (404, "Resource not found"),
|
||||
InvalidOperationException ioe => (409, ioe.Message),
|
||||
_ => (500, "An unexpected error occurred")
|
||||
};
|
||||
|
||||
context.Response.StatusCode = statusCode;
|
||||
context.Response.ContentType = "application/json";
|
||||
|
||||
var response = new ApiResponse<object>
|
||||
{
|
||||
Success = false,
|
||||
Error = error
|
||||
};
|
||||
|
||||
await context.Response.WriteAsJsonAsync(response);
|
||||
}
|
||||
}
|
||||
|
||||
// EN: Register in Program.cs / VI: Đăng ký trong Program.cs
|
||||
app.UseMiddleware<ExceptionHandlerMiddleware>();
|
||||
```
|
||||
|
||||
## OpenAPI/Swagger Configuration / Cấu Hình OpenAPI/Swagger
|
||||
|
||||
```csharp
|
||||
// EN: Configure Swagger in Program.cs
|
||||
// VI: Cấu hình Swagger trong Program.cs
|
||||
builder.Services.AddSwaggerGen(options =>
|
||||
{
|
||||
options.SwaggerDoc("v1", new OpenApiInfo
|
||||
{
|
||||
Title = "User Service API",
|
||||
Version = "v1",
|
||||
Description = "User management endpoints for GoodGo platform"
|
||||
});
|
||||
|
||||
// EN: Enable XML comments / VI: Bật XML comments
|
||||
var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
|
||||
var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
|
||||
options.IncludeXmlComments(xmlPath);
|
||||
|
||||
// EN: Add JWT authentication / VI: Thêm JWT authentication
|
||||
options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
|
||||
{
|
||||
Description = "JWT Authorization header using the Bearer scheme",
|
||||
Name = "Authorization",
|
||||
In = ParameterLocation.Header,
|
||||
Type = SecuritySchemeType.ApiKey,
|
||||
Scheme = "Bearer"
|
||||
});
|
||||
|
||||
options.AddSecurityRequirement(new OpenApiSecurityRequirement
|
||||
{
|
||||
{
|
||||
new OpenApiSecurityScheme
|
||||
{
|
||||
Reference = new OpenApiReference
|
||||
{
|
||||
Type = ReferenceType.SecurityScheme,
|
||||
Id = "Bearer"
|
||||
}
|
||||
},
|
||||
Array.Empty<string>()
|
||||
}
|
||||
});
|
||||
|
||||
// EN: Enable annotations / VI: Bật annotations
|
||||
options.EnableAnnotations();
|
||||
});
|
||||
```
|
||||
|
||||
## API Versioning Configuration / Cấu Hình API Versioning
|
||||
|
||||
```csharp
|
||||
// EN: Configure API versioning in Program.cs
|
||||
// VI: Cấu hình API versioning trong Program.cs
|
||||
builder.Services.AddApiVersioning(options =>
|
||||
{
|
||||
options.DefaultApiVersion = new ApiVersion(1, 0);
|
||||
options.AssumeDefaultVersionWhenUnspecified = true;
|
||||
options.ReportApiVersions = true;
|
||||
options.ApiVersionReader = new UrlSegmentApiVersionReader();
|
||||
})
|
||||
.AddApiExplorer(options =>
|
||||
{
|
||||
options.GroupNameFormat = "'v'VVV";
|
||||
options.SubstituteApiVersionInUrl = true;
|
||||
});
|
||||
```
|
||||
|
||||
## Resources / Tài Nguyên
|
||||
|
||||
- [ASP.NET Core Web API Best Practices](https://docs.microsoft.com/en-us/aspnet/core/web-api/)
|
||||
- [MediatR Documentation](https://github.com/jbogard/MediatR)
|
||||
- [FluentValidation Documentation](https://docs.fluentvalidation.net/)
|
||||
- [Swashbuckle Documentation](https://github.com/domaindrivendev/Swashbuckle.AspNetCore)
|
||||
Reference in New Issue
Block a user