16 KiB
16 KiB
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
/// <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
/// <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
/// <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
/// <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
/// <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
/// <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
/// <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
/// <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
// 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
// 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;
});