AgentSkillsCN

asp-rules

Beyond8 项目中的 ASP.NET Core 最佳实践与编码规范

SKILL.md
--- frontmatter
name: asp-rules
description: ASP.NET Core Best Practices and Coding Standards for Beyond8 project

ASP.NET Core Best Practices Rules

Overview

Best practices and coding standards for ASP.NET Core development in the Beyond8 project. Follow these rules to maintain consistency, security, and code quality across all services.


1. API Design (Minimal APIs)

Endpoint Organization

  • Always organize endpoints using MapGroup with versioning (e.g., /api/v1/...)
  • Use static extension methods for API mapping (e.g., MapAuthApi())
  • Add descriptive tags, names, and descriptions for OpenAPI documentation
  • Always specify return types using .Produces<>() for proper OpenAPI generation
  • Explicitly use AllowAnonymous() or RequireAuthorization() on each endpoint
  • Apply rate limiting where appropriate using RequireRateLimiting()

Request/Response Handling

  • Always use ApiResponse<T> wrapper for consistent API responses
  • Return appropriate HTTP status codes: 200 for success, 400 for bad requests, 401 for unauthorized, 404 for not found, 500 for server errors
  • Always handle both success and failure cases in endpoint handlers
  • Never return raw exceptions or unhandled errors to clients

2. Response Consistency

ApiResponse Pattern

  • Always wrap service responses in ApiResponse<T> type
  • Use factory methods: SuccessResponse(), FailureResponse(), SuccessPagedResponse()
  • Include meaningful messages in all responses for user understanding
  • Use SuccessPagedResponse() for paginated data with metadata

Response Examples

  • Success: ApiResponse<T>.SuccessResponse(data, message)
  • Failure: ApiResponse<T>.FailureResponse(message)
  • Paginated: ApiResponse<List<T>>.SuccessPagedResponse(items, totalItems, pageNumber, pageSize, message)

3. Error Handling

Global Exception Middleware

  • Always use GlobalExceptionsMiddleware for centralized error handling
  • Log all exceptions with appropriate log levels before handling
  • Map exceptions to appropriate HTTP status codes using switch expressions
  • Never expose sensitive error details to clients in production environment

Service Layer Error Handling

  • Catch and handle specific exceptions in service methods
  • Return ApiResponse<T>.FailureResponse() instead of throwing exceptions for business logic errors
  • Always log errors with structured logging using ILogger
  • Never use generic catch blocks without logging the exception
  • Use try-catch blocks for error handling, not for flow control

Exception Mapping

  • UnauthorizedAccessException maps to HTTP 401 Unauthorized
  • ArgumentException maps to HTTP 400 Bad Request
  • KeyNotFoundException maps to HTTP 404 Not Found
  • All other exceptions map to HTTP 500 Internal Server Error

4. Validation

FluentValidation (Recommended)

  • Always use FluentValidation for request validation in Minimal APIs
  • FluentValidation provides better type safety, testability, and separation of concerns
  • Create validators in Application/Validators folder, organized by domain (e.g., Auth, Users)
  • Register validators using AddValidatorsFromAssemblyContaining<T>() in DI container
  • Inject validators into endpoint handlers using IValidator<TRequest>
  • Use ValidationExtensions.ValidateRequest() method for consistent validation handling

Validator Rules

  • Use NotEmpty() instead of Required for non-nullable types
  • Provide meaningful error messages in Vietnamese for user-facing validation
  • Common rules: NotEmpty(), EmailAddress(), MinimumLength(), MaximumLength(), Matches(), Equal(), NotEqual()
  • Use Length(exact) for fixed-length strings like OTP codes
  • Chain multiple rules using method chaining for readability
  • Use Matches() with regex for complex patterns (passwords, phone numbers)

Validation in Endpoints

  • Always validate request DTOs at the beginning of endpoint handlers
  • Pattern: if (!request.ValidateRequest(validator, out var validationResult)) return validationResult!;
  • Validation errors are automatically formatted as ApiResponse<object>.FailureResponse()
  • Never process requests without validation

Validator Examples

csharp
public class RegisterRequestValidator : AbstractValidator<RegisterRequest>
{
    public RegisterRequestValidator()
    {
        RuleFor(x => x.Email)
            .NotEmpty().WithMessage("Email không được để trống")
            .EmailAddress().WithMessage("Email không hợp lệ")
            .MaximumLength(256).WithMessage("Email không được vượt quá 256 ký tự");

        RuleFor(x => x.Password)
            .NotEmpty().WithMessage("Password không được để trống")
            .MinimumLength(8).WithMessage("Password tối thiểu 8 ký tự")
            .Matches(@"^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)")
            .WithMessage("Password phải có ít nhất 1 chữ thường, 1 chữ hoa và 1 số");
    }
}

5. Security Best Practices

Password Handling

  • Always hash passwords using PasswordHasher<User> before storing
  • Never store plain text passwords in database
  • Use strong password requirements: minimum length 8 characters, complexity rules
  • Validate password strength in service layer before hashing

JWT Authentication

  • Store JWT configuration in appsettings.json, never hardcode
  • Use secure token expiration times (typically 15-60 minutes for access tokens)
  • Always implement refresh token mechanism for long-lived sessions
  • Validate tokens on all protected endpoints using RequireAuthorization()

Authorization

  • Use RequireAuthorization() on all protected endpoints
  • Use role-based authorization when user roles are defined
  • Always validate user permissions in service layer, not just in API layer
  • Never trust client-side authentication state, always verify server-side

6. Dependency Injection

Service Registration

  • Use extension methods for service registration (e.g., AddApplicationServices())
  • Register services with appropriate lifetimes based on usage:
    • Use Scoped for services with database context (IUnitOfWork, services)
    • Use Transient for stateless services without dependencies on context
    • Use Singleton only for thread-safe services (caching, configuration)
  • Always register repositories through Unit of Work pattern
  • Group related service registrations in extension methods

Constructor Injection

  • Use primary constructors (C# 12) when available for cleaner code
  • Always inject interfaces, never concrete classes
  • Never use service locator pattern or static service resolution
  • Keep constructor parameter lists manageable (consider refactoring if too many)

7. Repository Pattern & Unit of Work

Repository Interface

  • Define repository interfaces in Domain layer
  • Implement repositories in Infrastructure layer
  • Use generic repository pattern (IGenericRepository<T>) for common CRUD operations
  • Add specific methods only when needed, avoid duplicating generic methods
  • Use Unit of Work pattern for transaction management across repositories

Database Operations

  • Always use async methods: FindOneAsync, FindAllAsync, AddAsync, UpdateAsync, SaveChangesAsync
  • Use transactions for operations that must succeed together
  • Handle database exceptions appropriately with specific error messages
  • Never use synchronous database methods (.Result, .Wait())

Unit of Work Pattern

  • Expose repositories as properties through IUnitOfWork interface
  • Implement SaveChangesAsync() in UnitOfWork for transaction control
  • Use IUnitOfWork for all database operations in service layer

8. Logging

Structured Logging

  • Always use ILogger<T> for logging in all services
  • Use structured logging with parameters, never string interpolation
  • Include relevant context in log messages using named parameters
  • Use appropriate log levels:
    • LogError for exceptions and critical errors
    • LogWarning for business logic issues and recoverable errors
    • LogInformation for important business events and milestones
    • LogDebug for detailed debugging information (development only)

Logging Examples

  • Good: logger.LogInformation("User registered successfully: {Email}", request.Email)
  • Good: logger.LogError(ex, "Error registering user with email {Email}", request.Email)
  • Bad: logger.LogInformation($"User registered successfully: {request.Email}")

9. Async/Await Patterns

Asynchronous Methods

  • Always use async/await for all I/O operations: database, HTTP, file system, external APIs
  • Return Task<T> or Task from async methods, never void
  • Use ConfigureAwait(false) only when necessary (rare in ASP.NET Core)
  • Never use .Result or .Wait() on async methods as it causes deadlocks
  • Use async all the way: if a method calls async, it should be async

Async Best Practices

  • Good: public async Task<ApiResponse<T>> MethodAsync() { await ... }
  • Bad: public Task<ApiResponse<T>> MethodAsync() { return task.Result; }
  • Always await async calls, don't return tasks directly unless intentionally

10. Clean Architecture Layers

Layer Responsibilities

  • Domain Layer: Contains entities, enums, repository interfaces, domain logic only
  • Application Layer: Contains DTOs, service interfaces and implementations, mappings
  • Infrastructure Layer: Contains database context, repository implementations, external services
  • API Layer: Contains endpoints, request/response handling, middleware configuration

Dependency Direction

  • Maintain strict dependency direction: API → Application → Domain ← Infrastructure
  • Keep Domain layer completely independent of all other layers
  • Never reference Infrastructure layer from Application layer
  • API layer can reference Application and Infrastructure, but not Domain directly

11. Entity Framework & Database

Entities

  • Always inherit from BaseEntity for common properties: Id, CreatedAt, UpdatedAt
  • Use [Required] attribute for required properties with meaningful error messages
  • Configure relationships explicitly in DbContext OnModelCreating method
  • Use [MaxLength] for string properties to define database column constraints

Migrations

  • Always create migrations for schema changes, never modify database manually
  • Apply migrations on application startup in Development environment
  • Use CI/CD pipeline for migration application in Production
  • Always review migration files before applying to ensure correctness

Database Context

  • Use DbContextFactory for creating contexts in migrations
  • Apply migrations using MigrateDbContextAsync<T>() extension method in Program.cs
  • Never commit sensitive connection strings to source control

12. Caching

Cache Service

  • Use ICacheService for all caching operations
  • Always set appropriate expiration times for cached data
  • Use consistent key naming conventions (e.g., "otp_register:{email}")
  • Consider cache invalidation strategy when updating data

Cache Examples

  • Key format: "prefix:identifier" (e.g., "otp_register:{email}")
  • Set: await cacheService.SetAsync(key, value, TimeSpan.FromMinutes(5))
  • Get: await cacheService.GetAsync<T>(key)
  • Always handle cache misses gracefully

13. Configuration

appsettings.json

  • Store all configuration in appsettings.json and appsettings.Development.json
  • Use strongly-typed configuration classes when possible
  • Never commit secrets or sensitive data to source control
  • Use User Secrets (Development) or environment variables (Production) for sensitive data

Connection Strings

  • Reference connection strings by name using constants (e.g., Const.IdentityServiceDatabase)
  • Store connection strings in appsettings.json, never hardcode
  • Use extension methods for database configuration (e.g., AddPostgresDatabase<T>())

14. Code Organization & Naming

File Structure

  • Group related files in folders: Dtos, Services, Entities, Repositories, Mappings
  • Use descriptive namespaces matching folder structure exactly
  • Keep files focused on single responsibility principle
  • Separate concerns: put interfaces in separate files from implementations

Naming Conventions

  • Use PascalCase for classes, methods, properties, interfaces
  • Use camelCase for local variables and method parameters
  • Prefix interfaces with I (e.g., IAuthService, IUserRepository)
  • Use descriptive names that clearly indicate purpose and responsibility
  • Use Async suffix for async methods (e.g., RegisterUserAsync)

Mapping

  • Create mapping extension methods in Application/Mappings folder
  • Keep mapping logic separate from business logic
  • Use consistent naming: ToEntity(), ToDto(), ToUserSimpleResponse()
  • Group mappings by domain entity (e.g., RegisterMappings, UserMappings)

15. Performance & Testing

Performance Best Practices

  • Use FindOneAsync instead of fetching all records and filtering in memory
  • Include only necessary data in queries, avoid SELECT * patterns
  • Use pagination for large datasets using SuccessPagedResponse()
  • Cache frequently accessed data that doesn't change often
  • Optimize database queries and ensure proper indexes exist

Testing Considerations

  • Design services to be testable using dependency injection and interfaces
  • Mock dependencies in unit tests using test doubles or mocks
  • Test business logic separately from infrastructure concerns
  • Use integration tests for database operations, unit tests for business logic

Quick Reference

Common Patterns

  • Service Response: ApiResponse<T>.SuccessResponse(data, message)
  • Error Response: ApiResponse<T>.FailureResponse(message)
  • Paginated Response: ApiResponse<List<T>>.SuccessPagedResponse(items, total, page, size, message)
  • Structured Logging: logger.LogInformation("Message with {Parameter}", value)
  • Async Query: await repository.FindOneAsync(x => x.Id == id)
  • Service Registration: builder.Services.AddScoped<IService, Service>()
  • Protected Endpoint: .RequireAuthorization()
  • Validation: [Required(ErrorMessage = "Error message")]

Service Lifetimes

  • Scoped: Services with database context (IUnitOfWork, application services)
  • Transient: Stateless services without context dependencies
  • Singleton: Thread-safe services (cache, configuration readers)

Checklist

Before submitting code, ensure:

  • All API endpoints use ApiResponse<T> wrapper
  • Services return ApiResponse<T> instead of throwing exceptions for business errors
  • All async methods use async/await properly, no .Result or .Wait()
  • DTOs have validation attributes with meaningful error messages in Vietnamese
  • Logging uses structured logging with parameters, not string interpolation
  • Dependencies are injected through constructors, not service locator
  • Repository pattern and Unit of Work are used for all data access
  • Clean architecture layers are respected (no circular dependencies)
  • Security best practices followed (password hashing, JWT, authorization)
  • Error handling is centralized in middleware
  • Configuration stored in appsettings.json, not hardcoded
  • Code follows consistent naming conventions (PascalCase, camelCase)
  • Database operations use async methods only