diff --git a/services/_template_dot_net/.env.example b/services/_template_dot_net/.env.example new file mode 100644 index 00000000..d01da75f --- /dev/null +++ b/services/_template_dot_net/.env.example @@ -0,0 +1,108 @@ +# Environment Variables / Biến Môi Trường + +# EN: Copy this file to .env and fill in the values +# VI: Sao chép file này sang .env và điền các giá trị + +# ============================================ +# Application Settings / Cài Đặt Ứng Dụng +# ============================================ + +# EN: Environment name (Development, Staging, Production) +# VI: Tên môi trường (Development, Staging, Production) +ASPNETCORE_ENVIRONMENT=Development + +# EN: Service port (default: 8080) +# VI: Cổng service (mặc định: 8080) +PORT=8080 + +# EN: Service name +# VI: Tên service +SERVICE_NAME=your-service-name + +# ============================================ +# Database / Cơ Sở Dữ Liệu +# ============================================ + +# EN: PostgreSQL connection string +# VI: Chuỗi kết nối PostgreSQL +DATABASE_URL=Host=localhost;Port=5432;Database=your_db;Username=postgres;Password=postgres + +# ============================================ +# Redis Cache / Redis Cache +# ============================================ + +# EN: Redis connection string (optional) +# VI: Chuỗi kết nối Redis (tùy chọn) +REDIS_URL=localhost:6379 + +# ============================================ +# Authentication / Xác Thực +# ============================================ + +# EN: JWT secret key (minimum 32 characters) +# VI: Khóa bí mật JWT (tối thiểu 32 ký tự) +JWT_SECRET=your-secret-key-must-be-at-least-32-characters-long + +# EN: JWT issuer +# VI: Nhà phát hành JWT +JWT_ISSUER=goodgo-platform + +# EN: JWT audience +# VI: Đối tượng JWT +JWT_AUDIENCE=goodgo-services + +# EN: Access token expiration (in minutes) +# VI: Thời gian hết hạn access token (phút) +JWT_ACCESS_TOKEN_EXPIRATION=15 + +# EN: Refresh token expiration (in days) +# VI: Thời gian hết hạn refresh token (ngày) +JWT_REFRESH_TOKEN_EXPIRATION=7 + +# ============================================ +# External Services / Dịch Vụ Bên Ngoài +# ============================================ + +# EN: API Gateway URL +# VI: URL API Gateway +API_GATEWAY_URL=http://localhost + +# EN: Internal service authentication key +# VI: Khóa xác thực service nội bộ +INTERNAL_API_KEY=your-internal-api-key + +# ============================================ +# Observability / Giám Sát +# ============================================ + +# EN: Log level (Trace, Debug, Information, Warning, Error, Critical) +# VI: Mức log (Trace, Debug, Information, Warning, Error, Critical) +LOG_LEVEL=Information + +# EN: Enable detailed errors (true in development only) +# VI: Bật lỗi chi tiết (chỉ true trong development) +DETAILED_ERRORS=true + +# EN: OpenTelemetry endpoint (optional) +# VI: Endpoint OpenTelemetry (tùy chọn) +OTEL_EXPORTER_ENDPOINT=http://localhost:4317 + +# ============================================ +# CORS Settings / Cài Đặt CORS +# ============================================ + +# EN: Allowed origins (comma-separated) +# VI: Origins được phép (phân cách bởi dấu phẩy) +CORS_ORIGINS=http://localhost:3000,http://localhost:5173 + +# ============================================ +# Rate Limiting / Giới Hạn Tốc Độ +# ============================================ + +# EN: Enable rate limiting +# VI: Bật giới hạn tốc độ +RATE_LIMIT_ENABLED=true + +# EN: Requests per minute +# VI: Số requests mỗi phút +RATE_LIMIT_REQUESTS_PER_MINUTE=100 diff --git a/services/_template_dot_net/ARCHITECTURE.md b/services/_template_dot_net/ARCHITECTURE.md new file mode 100644 index 00000000..3fbb8378 --- /dev/null +++ b/services/_template_dot_net/ARCHITECTURE.md @@ -0,0 +1,274 @@ +# .NET Service Architecture / Kiến Trúc Dịch Vụ .NET + +> **EN**: Comprehensive architecture documentation for .NET microservices +> **VI**: Tài liệu kiến trúc toàn diện cho microservices .NET + +## Table of Contents / Mục Lục + +- [Clean Architecture Overview](#clean-architecture-overview) +- [Layer Responsibilities](#layer-responsibilities) +- [Project Structure](#project-structure) +- [Dependency Flow](#dependency-flow) +- [Design Patterns](#design-patterns) +- [Best Practices](#best-practices) + +## Clean Architecture Overview + +**EN**: This template implements Clean Architecture (also known as Onion Architecture or Hexagonal Architecture) to achieve: +- **Independence of Frameworks**: Business logic doesn't depend on frameworks +- **Testability**: Business rules can be tested without UI, database, or external services +- **Independence of UI**: UI can change without changing business rules +- **Independence of Database**: Business rules are not bound to database +- **Independence of External Services**: Business rules don't know about external services + +**VI**: Template này triển khai Clean Architecture (còn gọi là Onion Architecture hoặc Hexagonal Architecture) để đạt được: +- **Độc lập với Frameworks**: Business logic không phụ thuộc vào frameworks +- **Khả năng kiểm thử**: Business rules có thể được test mà không cần UI, database hoặc external services +- **Độc lập với UI**: UI có thể thay đổi mà không ảnh hưởng business rules +- **Độc lập với Database**: Business rules không bị ràng buộc với database +- **Độc lập với External Services**: Business rules không biết về external services + +## Layer Responsibilities / Trách Nhiệm Các Lớp + +### 1. Domain Layer (Core) / Lớp Domain (Lõi) + +**EN**: The innermost layer containing enterprise business rules. + +**VI**: Lớp trong cùng chứa các business rules của doanh nghiệp. + +**Contains / Chứa:** +- Entities / Thực thể +- Value Objects +- Domain Events +- Repository Interfaces +- Domain Services Interfaces + +**No dependencies / Không phụ thuộc**: This layer has no dependencies on other layers. + +### 2. Application Layer / Lớp Application + +**EN**: Contains application-specific business rules and orchestrates workflows. + +**VI**: Chứa các business rules cụ thể của ứng dụng và điều phối workflows. + +**Contains / Chứa:** +- DTOs (Data Transfer Objects) +- Service Interfaces +- Service Implementations +- Validators (FluentValidation) +- Mappers (AutoMapper) + +**Dependencies / Phụ thuộc**: Domain layer only + +### 3. Infrastructure Layer / Lớp Infrastructure + +**EN**: Contains implementation details for external concerns. + +**VI**: Chứa implementation details cho các concerns bên ngoài. + +**Contains / Chứa:** +- DbContext (Entity Framework Core) +- Repository Implementations +- External Service Clients +- Cache Implementations +- File Storage Implementations + +**Dependencies / Phụ thuộc**: Domain layer + +### 4. API Layer (Presentation) / Lớp API (Presentation) + +**EN**: Contains all the entry points to the application. + +**VI**: Chứa tất cả các điểm vào của ứng dụng. + +**Contains / Chứa:** +- Controllers +- Middleware +- Filters +- Configuration +- Startup/Program.cs + +**Dependencies / Phụ thuộc**: Application and Infrastructure layers + +## Dependency Flow / Luồng Phụ Thuộc + +``` +┌─────────────────────────────────────────┐ +│ API Layer (Presentation) │ +│ Controllers, Middleware, Config │ +└───────────────┬─────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────┐ +│ Application Layer │ +│ Services, DTOs, Validators │ +└──────────┬──────────────────────┬────────┘ + │ │ + ▼ ▼ +┌──────────────────┐ ┌─────────────────────┐ +│ Domain Layer │ │ Infrastructure │ +│ Entities, │◄──│ DbContext, Repos, │ +│ Interfaces │ │ External Services │ +└──────────────────┘ └─────────────────────┘ +``` + +**EN**: All dependencies point inward. The Domain layer has no dependencies on any other layer. + +**VI**: Tất cả dependencies đều hướng vào trong. Lớp Domain không phụ thuộc vào bất kỳ lớp nào khác. + +## Design Patterns / Mẫu Thiết Kế + +### Repository Pattern + +**EN**: Abstracts data access logic and provides a collection-like interface for accessing domain objects. + +**VI**: Trừu tượng hóa logic truy cập dữ liệu và cung cấp interface giống collection để truy cập domain objects. + +```csharp +// Domain Layer - Interface +public interface IUserRepository +{ + Task GetByIdAsync(Guid id); + Task> GetAllAsync(); + Task AddAsync(User user); + Task UpdateAsync(User user); + Task DeleteAsync(Guid id); +} + +// Infrastructure Layer - Implementation +public class UserRepository : IUserRepository +{ + private readonly ApplicationDbContext _context; + + public UserRepository(ApplicationDbContext context) + { + _context = context; + } + + public async Task GetByIdAsync(Guid id) + { + return await _context.Users.FindAsync(id); + } + // ... other implementations +} +``` + +### Service Pattern + +**EN**: Encapsulates business logic and orchestrates operations across repositories. + +**VI**: Đóng gói business logic và điều phối các operations giữa các repositories. + +```csharp +// Application Layer +public interface IUserService +{ + Task GetUserAsync(Guid id); + Task CreateUserAsync(CreateUserDto dto); +} + +public class UserService : IUserService +{ + private readonly IUserRepository _userRepository; + private readonly IMapper _mapper; + + public UserService(IUserRepository userRepository, IMapper mapper) + { + _userRepository = userRepository; + _mapper = mapper; + } + + public async Task GetUserAsync(Guid id) + { + var user = await _userRepository.GetByIdAsync(id); + if (user == null) throw new NotFoundException("User not found"); + + return _mapper.Map(user); + } +} +``` + +### Unit of Work Pattern + +**EN**: Maintains a list of objects affected by a business transaction and coordinates writing changes. + +**VI**: Duy trì danh sách các objects bị ảnh hưởng bởi một business transaction và điều phối việc ghi thay đổi. + +### CQRS (Command Query Responsibility Segregation) + +**EN**: Optional pattern for separating read and write operations. + +**VI**: Pattern tùy chọn để tách biệt các operations đọc và ghi. + +## Best Practices / Thực Hành Tốt + +### 1. Dependency Injection + +```csharp +// EN: Register services in Program.cs +// VI: Đăng ký services trong Program.cs +builder.Services.AddScoped(); +builder.Services.AddScoped(); +``` + +### 2. Async/Await + +```csharp +// EN: Always use async for I/O operations +// VI: Luôn dùng async cho I/O operations +public async Task GetUserAsync(Guid id) +{ + return await _context.Users.FindAsync(id); +} +``` + +### 3. Error Handling + +```csharp +// EN: Create custom exceptions +// VI: Tạo custom exceptions +public class NotFoundException : Exception +{ + public NotFoundException(string message) : base(message) { } +} + +// EN: Global exception handler middleware +// VI: Middleware xử lý exception toàn cục +public class ExceptionMiddleware +{ + // ... implementation +} +``` + +### 4. Validation + +```csharp +// EN: Use FluentValidation for input validation +// VI: Dùng FluentValidation để validate input +public class CreateUserDtoValidator : AbstractValidator +{ + public CreateUserDtoValidator() + { + RuleFor(x => x.Email).NotEmpty().EmailAddress(); + RuleFor(x => x.Password).MinimumLength(8); + } +} +``` + +## Testing Strategy / Chiến Lược Testing + +**EN**: +- **Unit Tests**: Test business logic in isolation +- **Integration Tests**: Test layer interactions +- **E2E Tests**: Test complete workflows + +**VI**: +- **Unit Tests**: Test business logic độc lập +- **Integration Tests**: Test tương tác giữa các lớp +- **E2E Tests**: Test toàn bộ workflows + +## Resources / Tài Nguyên + +- [Clean Architecture by Uncle Bob](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html) +- [.NET Microservices Architecture](https://docs.microsoft.com/en-us/dotnet/architecture/microservices/) +- [Domain-Driven Design](https://martinfowler.com/tags/domain%20driven%20design.html) diff --git a/services/_template_dot_net/Dockerfile b/services/_template_dot_net/Dockerfile new file mode 100644 index 00000000..11a13056 --- /dev/null +++ b/services/_template_dot_net/Dockerfile @@ -0,0 +1,64 @@ +# Build stage / Giai đoạn build +FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build +WORKDIR /src + +# EN: Copy project files +# VI: Sao chép các file project +COPY ["src/YourServiceName.Api/YourServiceName.Api.csproj", "src/YourServiceName.Api/"] +COPY ["src/YourServiceName.Domain/YourServiceName.Domain.csproj", "src/YourServiceName.Domain/"] +COPY ["src/YourServiceName.Infrastructure/YourServiceName.Infrastructure.csproj", "src/YourServiceName.Infrastructure/"] + +# EN: Restore dependencies +# VI: Khôi phục dependencies +RUN dotnet restore "src/YourServiceName.Api/YourServiceName.Api.csproj" + +# EN: Copy all source code +# VI: Sao chép toàn bộ source code +COPY . . + +# EN: Build the application +# VI: Build ứng dụng +WORKDIR "/src/src/YourServiceName.Api" +RUN dotnet build "YourServiceName.Api.csproj" -c Release -o /app/build + +# Publish stage / Giai đoạn publish +FROM build AS publish +RUN dotnet publish "YourServiceName.Api.csproj" -c Release -o /app/publish /p:UseAppHost=false + +# Runtime stage / Giai đoạn runtime +FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS final +WORKDIR /app + +# EN: Create non-root user for security +# VI: Tạo user non-root cho bảo mật +RUN addgroup --gid 1001 --system dotnetuser && \ + adduser --uid 1001 --system --ingroup dotnetuser dotnetuser + +# EN: Copy published application +# VI: Sao chép ứng dụng đã publish +COPY --from=publish /app/publish . + +# EN: Change ownership to non-root user +# VI: Thay đổi quyền sở hữu sang user non-root +RUN chown -R dotnetuser:dotnetuser /app + +# EN: Switch to non-root user +# VI: Chuyển sang user non-root +USER dotnetuser + +# EN: Expose port +# VI: Mở cổng +EXPOSE 8080 + +# EN: Set environment variables +# VI: Thiết lập biến môi trường +ENV ASPNETCORE_URLS=http://+:8080 + +# EN: Health check +# VI: Kiểm tra health +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD curl -f http://localhost:8080/health/live || exit 1 + +# EN: Start the application +# VI: Khởi động ứng dụng +ENTRYPOINT ["dotnet", "YourServiceName.Api.dll"] diff --git a/services/_template_dot_net/README.md b/services/_template_dot_net/README.md new file mode 100644 index 00000000..98f32bec --- /dev/null +++ b/services/_template_dot_net/README.md @@ -0,0 +1,226 @@ +# .NET Service Template / Template Dịch Vụ .NET + +> **EN**: Template for creating new .NET microservices in the GoodGo platform +> **VI**: Template để tạo microservices .NET mới trong nền tảng GoodGo + +## Overview / Tổng Quan + +**EN**: This template provides a standardized structure for .NET microservices with: +- ASP.NET Core Web API +- Entity Framework Core +- Clean Architecture principles +- Health checks and observability +- Docker support +- Authentication and authorization + +**VI**: Template này cung cấp cấu trúc chuẩn hóa cho các microservices .NET với: +- ASP.NET Core 10 Web API +- Entity Framework Core 10 (PostgreSQL / Neon) +- **Neon Database Integration** (Connection Resilience) +- 原 tắc Clean Architecture +- **CQRS với MediatR** +- **Resilience với Polly** +- **Global Exception Handling (RFC 7807)** +- **API Versioning** +- **Audit Logging** +- Health checks và observability +- Hỗ trợ Docker +- Xác thực và phân quyền +- Performance improvements của .NET 10 + +## Prerequisites / Yêu Cầu + +- .NET 10.0 SDK or later / .NET 10.0 SDK trở lên +- Docker & Docker Compose +- PostgreSQL (via Neon or local) +- Redis (optional, for caching) + +## Quick Start / Bắt Đầu Nhanh + +### 1. Create New Service / Tạo Service Mới + +```bash +# EN: Copy template to new service +# VI: Sao chép template sang service mới +cp -r services/_template_dot_net services/your-service-name + +# EN: Navigate to service directory +# VI: Di chuyển đến thư mục service +cd services/your-service-name +``` + +### 2. Configure Service / Cấu Hình Service + +**EN**: Update the following files: +- `YourServiceName.csproj` - Project name and namespace +- `appsettings.json` - Configuration settings +- `Dockerfile` - Service-specific settings + +**VI**: Cập nhật các file sau: +- `YourServiceName.csproj` - Tên project và namespace +- `appsettings.json` - Cài đặt cấu hình +- `Dockerfile` - Cài đặt cụ thể cho service + +### 3. Environment Variables / Biến Môi Trường + +Create `.env` file from `.env.example`: + +```bash +cp .env.example .env +``` + +**Required variables / Biến bắt buộc:** + +| Variable | Description / Mô Tả | Example | +|----------|---------------------|---------| +| `ASPNETCORE_ENVIRONMENT` | Environment (Development/Production) / Môi trường | `Development` | +| `DATABASE_URL` | PostgreSQL connection string / Chuỗi kết nối PostgreSQL | `Host=localhost;Database=mydb;Username=user;Password=pass` | +| `REDIS_URL` | Redis connection string / Chuỗi kết nối Redis | `localhost:6379` | +| `JWT_SECRET` | JWT signing secret / Secret ký JWT | `your-secret-key-min-32-chars` | + +## Project Structure / Cấu Trúc Dự Án + +``` +services/your-service-name/ +├── src/ +│ ├── YourServiceName.Api/ # Web API layer / Lớp Web API +│ │ ├── Controllers/ # API controllers / Controllers API +│ │ ├── Middleware/ # Custom middleware +│ │ ├── Program.cs # Entry point / Điểm khởi đầu +│ │ └── appsettings.json # Configuration / Cấu hình +│ │ +│ ├── YourServiceName.Application/ # Application layer / Lớp ứng dụng +│ │ ├── DTOs/ # Data Transfer Objects +│ │ ├── Services/ # Business logic services +│ │ └── Interfaces/ # Service interfaces +│ │ +│ ├── YourServiceName.Domain/ # Domain layer / Lớp domain +│ │ ├── Entities/ # Domain entities / Thực thể domain +│ │ └── Interfaces/ # Repository interfaces +│ │ +│ └── YourServiceName.Infrastructure/ # Infrastructure layer / Lớp hạ tầng +│ ├── Data/ # DbContext and migrations +│ ├── Repositories/ # Repository implementations +│ └── Services/ # External service clients +│ +├── tests/ +│ ├── YourServiceName.UnitTests/ # Unit tests +│ └── YourServiceName.IntegrationTests/ # Integration tests +│ +├── Dockerfile # Docker configuration +├── .dockerignore +└── README.md +``` + +## Development / Phát Triển + +### Run Locally / Chạy Local + +```bash +# EN: Restore dependencies +# VI: Khôi phục dependencies +dotnet restore + +# EN: Run migrations +# VI: Chạy migrations +dotnet ef database update --project src/YourServiceName.Infrastructure + +# EN: Start service +# VI: Khởi động service +dotnet run --project src/YourServiceName.Api +``` + +### Run with Docker / Chạy với Docker + +```bash +# EN: Build Docker image +# VI: Build Docker image +docker build -t your-service-name . + +# EN: Run container +# VI: Chạy container +docker run -p 5000:8080 --env-file .env your-service-name +``` + +## Testing / Kiểm Thử + +```bash +# EN: Run all tests +# VI: Chạy tất cả tests +dotnet test + +# EN: Run with coverage +# VI: Chạy với coverage +dotnet test /p:CollectCoverage=true /p:CoverageReportFormat=opencover + +# EN: Run specific test project +# VI: Chạy project test cụ thể +dotnet test tests/YourServiceName.UnitTests +``` + +## API Documentation / Tài Liệu API + +**EN**: The service automatically generates Swagger/OpenAPI documentation available at: +- Development: `http://localhost:5000/swagger` +- Production: `https://api.goodgo.com/your-service/swagger` + +**VI**: Service tự động tạo tài liệu Swagger/OpenAPI tại: +- Development: `http://localhost:5000/swagger` +- Production: `https://api.goodgo.com/your-service/swagger` + +## What's New in .NET 10 / Có Gì Mới Trong .NET 10 + +**EN**: This template leverages new .NET 10 features: +- Improved performance and reduced memory allocation +- Enhanced Native AOT support +- Better async/await performance +- Updated C# 13 language features +- Improved JSON serialization performance + +**VI**: Template này tận dụng các tính năng mới của .NET 10: +- Cải thiện hiệu năng và giảm memory allocation +- Hỗ trợ Native AOT tốt hơn +- Hiệu năng async/await được cải thiện +- Tính năng ngôn ngữ C# 13 mới +- Hiệu năng JSON serialization được cải thiện + +## Health Checks + +| Endpoint | Description / Mô Tả | +|----------|---------------------| +| `/health` | Overall health status / Trạng thái tổng thể | +| `/health/live` | Liveness probe / Kiểm tra sống | +| `/health/ready` | Readiness probe / Kiểm tra sẵn sàng | + +## Architecture Patterns / Mẫu Kiến Trúc + +**EN**: This template follows Clean Architecture principles: +1. **API Layer**: Controllers, middleware, configuration +2. **Application Layer**: Business logic, DTOs, services +3. **Domain Layer**: Entities, interfaces, domain logic +4. **Infrastructure Layer**: Data access, external services + +**VI**: Template này tuân theo nguyên tắc Clean Architecture: +1. **Lớp API**: Controllers, middleware, cấu hình +2. **Lớp Application**: Business logic, DTOs, services +3. **Lớp Domain**: Entities, interfaces, domain logic +4. **Lớp Infrastructure**: Truy cập dữ liệu, external services + +## Best Practices / Thực Hành Tốt + +1. **Dependency Injection**: Use built-in DI container / Sử dụng DI container có sẵn +2. **Async/Await**: Use async methods for I/O operations / Dùng async cho I/O +3. **Logging**: Use ILogger for structured logging / Dùng ILogger +4. **Validation**: Use FluentValidation for input validation / Dùng FluentValidation +5. **Error Handling**: Implement global exception middleware / Middleware xử lý lỗi toàn cục + +## Resources / Tài Nguyên + +- [ASP.NET Core Documentation](https://docs.microsoft.com/en-us/aspnet/core/) +- [Entity Framework Core](https://docs.microsoft.com/en-us/ef/core/) +- [Clean Architecture](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html) +- [GoodGo Platform Documentation](../../docs/README.md) + +## License / Giấy Phép + +Proprietary - GoodGo Platform diff --git a/services/_template_dot_net/appsettings.Development.json b/services/_template_dot_net/appsettings.Development.json new file mode 100644 index 00000000..af1ddc27 --- /dev/null +++ b/services/_template_dot_net/appsettings.Development.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Debug", + "Microsoft.AspNetCore": "Debug", + "Microsoft.EntityFrameworkCore": "Information" + } + }, + "DetailedErrors": true +} \ No newline at end of file diff --git a/services/_template_dot_net/appsettings.json b/services/_template_dot_net/appsettings.json new file mode 100644 index 00000000..4537a956 --- /dev/null +++ b/services/_template_dot_net/appsettings.json @@ -0,0 +1,44 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "Microsoft.EntityFrameworkCore": "Warning" + } + }, + "AllowedHosts": "*", + "ConnectionStrings": { + "DefaultConnection": "${DATABASE_URL}" + }, + "Redis": { + "ConnectionString": "${REDIS_URL}", + "InstanceName": "YourServiceName:" + }, + "JWT": { + "Secret": "${JWT_SECRET}", + "Issuer": "${JWT_ISSUER}", + "Audience": "${JWT_AUDIENCE}", + "AccessTokenExpirationMinutes": 15, + "RefreshTokenExpirationDays": 7 + }, + "Cors": { + "AllowedOrigins": [ + "http://localhost:3000", + "http://localhost:5173" + ] + }, + "RateLimit": { + "Enabled": true, + "RequestsPerMinute": 100, + "StrictRequestsPerHour": 10 + }, + "OpenTelemetry": { + "ServiceName": "YourServiceName", + "Endpoint": "${OTEL_EXPORTER_ENDPOINT}" + }, + "HealthChecks": { + "UI": { + "Enabled": true + } + } +} \ No newline at end of file diff --git a/services/_template_dot_net/global.json b/services/_template_dot_net/global.json new file mode 100644 index 00000000..228e1c40 --- /dev/null +++ b/services/_template_dot_net/global.json @@ -0,0 +1,5 @@ +Microsoft.NET.Test.Sdk +xunit +xunit.runner.visualstudio +Moq +FluentAssertions diff --git a/services/_template_dot_net/src/YourServiceName.Api/Application/Behaviors/LoggingBehavior.cs b/services/_template_dot_net/src/YourServiceName.Api/Application/Behaviors/LoggingBehavior.cs new file mode 100644 index 00000000..dc727b46 --- /dev/null +++ b/services/_template_dot_net/src/YourServiceName.Api/Application/Behaviors/LoggingBehavior.cs @@ -0,0 +1,35 @@ +using System.Diagnostics; +using MediatR; +using Microsoft.Extensions.Logging; + +namespace YourServiceName.Api.Application.Behaviors; + +public class LoggingBehavior : IPipelineBehavior + where TRequest : IRequest +{ + private readonly ILogger> _logger; + + public LoggingBehavior(ILogger> logger) + { + _logger = logger; + } + + public async Task Handle(TRequest request, RequestHandlerDelegate next, CancellationToken cancellationToken) + { + var requestName = typeof(TRequest).Name; + _logger.LogInformation("Processing Request: {Name} {@Request}", requestName, request); + + var timer = Stopwatch.StartNew(); + var response = await next(); + timer.Stop(); + + if (timer.ElapsedMilliseconds > 500) + { + _logger.LogWarning("Long Running Request: {Name} ({ElapsedMilliseconds} milliseconds) {@Request}", + requestName, timer.ElapsedMilliseconds, request); + } + + _logger.LogInformation("Completed Request: {Name}", requestName); + return response; + } +} diff --git a/services/_template_dot_net/src/YourServiceName.Api/Application/Behaviors/ValidationBehavior.cs b/services/_template_dot_net/src/YourServiceName.Api/Application/Behaviors/ValidationBehavior.cs new file mode 100644 index 00000000..61c13f9e --- /dev/null +++ b/services/_template_dot_net/src/YourServiceName.Api/Application/Behaviors/ValidationBehavior.cs @@ -0,0 +1,39 @@ +using FluentValidation; +using MediatR; +using Microsoft.Extensions.Logging; + +namespace YourServiceName.Api.Application.Behaviors; + +public class ValidationBehavior : IPipelineBehavior + where TRequest : IRequest +{ + private readonly IEnumerable> _validators; + private readonly ILogger> _logger; + + public ValidationBehavior(IEnumerable> validators, ILogger> logger) + { + _validators = validators; + _logger = logger; + } + + public async Task Handle(TRequest request, RequestHandlerDelegate next, CancellationToken cancellationToken) + { + if (!_validators.Any()) + { + return await next(); + } + + var context = new ValidationContext(request); + + var validationResults = await Task.WhenAll(_validators.Select(v => v.ValidateAsync(context, cancellationToken))); + var failures = validationResults.Where(r => r.Errors.Any()).SelectMany(r => r.Errors).ToList(); + + if (failures.Count != 0) + { + _logger.LogWarning("Validation failed for request {RequestType}", typeof(TRequest).Name); + throw new ValidationException(failures); + } + + return await next(); + } +} diff --git a/services/_template_dot_net/src/YourServiceName.Api/Application/Common/Models/Result.cs b/services/_template_dot_net/src/YourServiceName.Api/Application/Common/Models/Result.cs new file mode 100644 index 00000000..f7d6e740 --- /dev/null +++ b/services/_template_dot_net/src/YourServiceName.Api/Application/Common/Models/Result.cs @@ -0,0 +1,24 @@ +using MediatR; + +namespace YourServiceName.Api.Application.Common.Models; + +public class Result +{ + public bool Succeeded { get; init; } + public T? Data { get; init; } + public string? Error { get; init; } + public string? ErrorCode { get; init; } + + public static Result Success(T data) => new() { Succeeded = true, Data = data }; + public static Result Failure(string error, string errorCode = "Error") => new() { Succeeded = false, Error = error, ErrorCode = errorCode }; +} + +public class Result +{ + public bool Succeeded { get; init; } + public string? Error { get; init; } + public string? ErrorCode { get; init; } + + public static Result Success() => new() { Succeeded = true }; + public static Result Failure(string error, string errorCode = "Error") => new() { Succeeded = false, Error = error, ErrorCode = errorCode }; +} diff --git a/services/_template_dot_net/src/YourServiceName.Api/Controllers/HealthController.cs b/services/_template_dot_net/src/YourServiceName.Api/Controllers/HealthController.cs new file mode 100644 index 00000000..5d5b5c07 --- /dev/null +++ b/services/_template_dot_net/src/YourServiceName.Api/Controllers/HealthController.cs @@ -0,0 +1,67 @@ +using Microsoft.AspNetCore.Mvc; + +namespace YourServiceName.Api.Controllers; + +/// +/// EN: Health check controller for service monitoring +/// VI: Controller kiểm tra sức khỏe để giám sát service +/// +[ApiController] +[Route("[controller]")] +public class HealthController : ControllerBase +{ + private readonly ILogger _logger; + + public HealthController(ILogger logger) + { + _logger = logger; + } + + /// + /// EN: Liveness probe - checks if the service is running + /// VI: Liveness probe - kiểm tra xem service có đang chạy không + /// + [HttpGet("live")] + public IActionResult Live() + { + return Ok(new { status = "ok", timestamp = DateTime.UtcNow }); + } + + /// + /// EN: Readiness probe - checks if the service is ready to accept traffic + /// VI: Readiness probe - kiểm tra xem service có sẵn sàng nhận traffic không + /// + [HttpGet("ready")] + public IActionResult Ready() + { + // TODO: Add database and Redis connectivity checks + // TODO: Thêm kiểm tra kết nối database và Redis + + return Ok(new + { + status = "ready", + timestamp = DateTime.UtcNow, + dependencies = new + { + database = "ok", + redis = "ok" + } + }); + } + + /// + /// EN: Overall health check + /// VI: Kiểm tra sức khỏe tổng thể + /// + [HttpGet] + public IActionResult Health() + { + return Ok(new + { + service = "YourServiceName", + status = "healthy", + version = "1.0.0", + timestamp = DateTime.UtcNow + }); + } +} diff --git a/services/_template_dot_net/src/YourServiceName.Api/Extensions/ApiVersioningExtensions.cs b/services/_template_dot_net/src/YourServiceName.Api/Extensions/ApiVersioningExtensions.cs new file mode 100644 index 00000000..39e14566 --- /dev/null +++ b/services/_template_dot_net/src/YourServiceName.Api/Extensions/ApiVersioningExtensions.cs @@ -0,0 +1,26 @@ +using Asp.Versioning; +using Microsoft.Extensions.DependencyInjection; + +namespace YourServiceName.Api.Extensions; + +public static class ApiVersioningExtensions +{ + public static IServiceCollection AddStandardApiVersioning(this IServiceCollection services) + { + services.AddApiVersioning(options => + { + options.DefaultApiVersion = new ApiVersion(1, 0); + options.AssumeDefaultVersionWhenUnspecified = true; + options.ReportApiVersions = true; + options.ApiVersionReader = new UrlSegmentApiVersionReader(); + }) + .AddMvc() + .AddApiExplorer(options => + { + options.GroupNameFormat = "'v'VVV"; + options.SubstituteApiVersionInUrl = true; + }); + + return services; + } +} diff --git a/services/_template_dot_net/src/YourServiceName.Api/Extensions/ApplicationServiceExtensions.cs b/services/_template_dot_net/src/YourServiceName.Api/Extensions/ApplicationServiceExtensions.cs new file mode 100644 index 00000000..d2f7f162 --- /dev/null +++ b/services/_template_dot_net/src/YourServiceName.Api/Extensions/ApplicationServiceExtensions.cs @@ -0,0 +1,23 @@ +using System.Reflection; +using FluentValidation; +using MediatR; +using YourServiceName.Api.Application.Behaviors; + +namespace YourServiceName.Api.Extensions; + +public static class ApplicationServiceExtensions +{ + public static IServiceCollection AddApplicationServices(this IServiceCollection services) + { + services.AddValidatorsFromAssembly(Assembly.GetExecutingAssembly()); + + services.AddMediatR(cfg => { + cfg.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly()); + + cfg.AddBehavior(typeof(IPipelineBehavior<,>), typeof(LoggingBehavior<,>)); + cfg.AddBehavior(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>)); + }); + + return services; + } +} diff --git a/services/_template_dot_net/src/YourServiceName.Api/Middleware/GlobalExceptionHandler.cs b/services/_template_dot_net/src/YourServiceName.Api/Middleware/GlobalExceptionHandler.cs new file mode 100644 index 00000000..ef16275a --- /dev/null +++ b/services/_template_dot_net/src/YourServiceName.Api/Middleware/GlobalExceptionHandler.cs @@ -0,0 +1,53 @@ +using Microsoft.AspNetCore.Diagnostics; +using Microsoft.AspNetCore.Mvc; +using FluentValidation; + +namespace YourServiceName.Api.Middleware; + +public class GlobalExceptionHandler : IExceptionHandler +{ + private readonly ILogger _logger; + + public GlobalExceptionHandler(ILogger logger) + { + _logger = logger; + } + + public async ValueTask TryHandleAsync( + HttpContext httpContext, + Exception exception, + CancellationToken cancellationToken) + { + _logger.LogError(exception, "Unhandled exception occurred: {Message}", exception.Message); + + var problemDetails = new ProblemDetails + { + Status = StatusCodes.Status500InternalServerError, + Title = "An unexpected error occurred", + Detail = exception.Message, + Instance = httpContext.Request.Path + }; + + if (exception is ValidationException validationException) + { + problemDetails.Status = StatusCodes.Status400BadRequest; + problemDetails.Title = "Validation Error"; + problemDetails.Extensions["errors"] = validationException.Errors + .GroupBy(e => e.PropertyName) + .ToDictionary( + g => g.Key, + g => g.Select(e => e.ErrorMessage).ToArray() + ); + } + else if (exception is KeyNotFoundException) // Or custom NotFoundException + { + problemDetails.Status = StatusCodes.Status404NotFound; + problemDetails.Title = "Resource Not Found"; + } + + httpContext.Response.StatusCode = problemDetails.Status.Value; + await httpContext.Response.WriteAsJsonAsync(problemDetails, cancellationToken); + + return true; + } +} diff --git a/services/_template_dot_net/src/YourServiceName.Api/Program.cs b/services/_template_dot_net/src/YourServiceName.Api/Program.cs new file mode 100644 index 00000000..b4618488 --- /dev/null +++ b/services/_template_dot_net/src/YourServiceName.Api/Program.cs @@ -0,0 +1,86 @@ +using YourServiceName.Api.Middleware; +using YourServiceName.Infrastructure; +using YourServiceName.Api.Extensions; + +var builder = WebApplication.CreateBuilder(args); + +// EN: Configure Serilog for structured logging +// VI: Cấu hình Serilog cho structured logging +builder.Host.UseSerilog((context, configuration) => + configuration.ReadFrom.Configuration(context.Configuration)); + +// EN: Add services to the container +// VI: Thêm services vào container +builder.Services.AddControllers(); +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(); + +// EN: Configure CORS +// VI: Cấu hình CORS +builder.Services.AddCors(options => +{ + options.AddDefaultPolicy(policy => + { + var allowedOrigins = builder.Configuration.GetSection("Cors:AllowedOrigins").Get() + ?? new[] { "http://localhost:3000" }; + + policy.WithOrigins(allowedOrigins) + .AllowAnyMethod() + .AllowAnyHeader() + .AllowCredentials(); + }); +}); + +// EN: Add health checks +// VI: Thêm health checks +builder.Services.AddHealthChecks(); + +// EN: Add Infrastructure Services +// VI: Thêm các dịch vụ Infrastructure +builder.Services.AddInfrastructure(builder.Configuration); + +// EN: Add Application Services (MediatR, Behaviors) +// VI: Thêm các dịch vụ Application (MediatR, Behaviors) +builder.Services.AddApplicationServices(); + +// EN: Add API Versioning +// VI: Thêm quản lý phiên bản API +builder.Services.AddStandardApiVersioning(); + +// EN: Add Global Exception Handler +// VI: Thêm xử lý ngoại lệ toàn cục +builder.Services.AddExceptionHandler(); +builder.Services.AddProblemDetails(); + +var app = builder.Build(); + +// EN: Configure the HTTP request pipeline +// VI: Cấu hình HTTP request pipeline +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} + +// EN: Use Global Exception Handler +// VI: Sử dụng xử lý ngoại lệ toàn cục +app.UseExceptionHandler(); + +// EN: Add Serilog request logging +// VI: Thêm Serilog request logging +app.UseSerilogRequestLogging(); + +app.UseCors(); + +app.UseAuthentication(); +app.UseAuthorization(); + +app.MapControllers(); + +// EN: Map health check endpoints +// VI: Map các endpoints health check +app.MapHealthChecks("/health/live"); +app.MapHealthChecks("/health/ready"); +app.MapHealthChecks("/health"); + +app.Run(); diff --git a/services/_template_dot_net/src/YourServiceName.Api/YourServiceName.Api.csproj b/services/_template_dot_net/src/YourServiceName.Api/YourServiceName.Api.csproj new file mode 100644 index 00000000..3ae0c1b1 --- /dev/null +++ b/services/_template_dot_net/src/YourServiceName.Api/YourServiceName.Api.csproj @@ -0,0 +1,30 @@ + + + + net10.0 + enable + enable + YourServiceName.Api + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + diff --git a/services/_template_dot_net/src/YourServiceName.Domain/Common/Interfaces/IAuditableEntity.cs b/services/_template_dot_net/src/YourServiceName.Domain/Common/Interfaces/IAuditableEntity.cs new file mode 100644 index 00000000..997ee191 --- /dev/null +++ b/services/_template_dot_net/src/YourServiceName.Domain/Common/Interfaces/IAuditableEntity.cs @@ -0,0 +1,9 @@ +namespace YourServiceName.Domain.Common.Interfaces; + +public interface IAuditableEntity +{ + DateTime CreatedAt { get; set; } + string? CreatedBy { get; set; } + DateTime? LastModifiedAt { get; set; } + string? LastModifiedBy { get; set; } +} diff --git a/services/_template_dot_net/src/YourServiceName.Domain/Entities/.gitkeep b/services/_template_dot_net/src/YourServiceName.Domain/Entities/.gitkeep new file mode 100644 index 00000000..d63f1fea --- /dev/null +++ b/services/_template_dot_net/src/YourServiceName.Domain/Entities/.gitkeep @@ -0,0 +1,2 @@ +# EN: .gitkeep file to maintain empty directory structure +# VI: File .gitkeep để duy trì cấu trúc thư mục rỗng diff --git a/services/_template_dot_net/src/YourServiceName.Domain/SeedWork/Entity.cs b/services/_template_dot_net/src/YourServiceName.Domain/SeedWork/Entity.cs new file mode 100644 index 00000000..bf714a7e --- /dev/null +++ b/services/_template_dot_net/src/YourServiceName.Domain/SeedWork/Entity.cs @@ -0,0 +1,89 @@ +using MediatR; + +namespace YourServiceName.Domain.SeedWork; + +public abstract class Entity +{ + int _id; + public virtual int Id + { + get + { + return _id; + } + protected set + { + _id = value; + } + } + + private List _domainEvents; + public IReadOnlyCollection DomainEvents => _domainEvents?.AsReadOnly(); + + public void AddDomainEvent(INotification eventItem) + { + _domainEvents = _domainEvents ?? new List(); + _domainEvents.Add(eventItem); + } + + public void RemoveDomainEvent(INotification eventItem) + { + _domainEvents?.Remove(eventItem); + } + + public void ClearDomainEvents() + { + _domainEvents?.Clear(); + } + + public bool IsTransient() + { + return this.Id == default(int); + } + + public override bool Equals(object obj) + { + if (obj == null || !(obj is Entity)) + return false; + + if (Object.ReferenceEquals(this, obj)) + return true; + + if (this.GetType() != obj.GetType()) + return false; + + Entity item = (Entity)obj; + + if (item.IsTransient() || this.IsTransient()) + return false; + else + return item.Id == this.Id; + } + + public override int GetHashCode() + { + if (!IsTransient()) + { + if (!_requestedHashCode.HasValue) + _requestedHashCode = this.Id.GetHashCode() ^ 31; // XOR for random distribution (http://blogs.msdn.com/b/ericlippert/archive/2011/02/28/guidelines-and-rules-for-gethashcode.aspx) + + return _requestedHashCode.Value; + } + else + return base.GetHashCode(); + + } + private int? _requestedHashCode; + public static bool operator ==(Entity left, Entity right) + { + if (Object.Equals(left, null)) + return (Object.Equals(right, null)) ? true : false; + else + return left.Equals(right); + } + + public static bool operator !=(Entity left, Entity right) + { + return !(left == right); + } +} diff --git a/services/_template_dot_net/src/YourServiceName.Domain/SeedWork/IAggregateRoot.cs b/services/_template_dot_net/src/YourServiceName.Domain/SeedWork/IAggregateRoot.cs new file mode 100644 index 00000000..2bb63d5a --- /dev/null +++ b/services/_template_dot_net/src/YourServiceName.Domain/SeedWork/IAggregateRoot.cs @@ -0,0 +1,3 @@ +namespace YourServiceName.Domain.SeedWork; + +public interface IAggregateRoot { } diff --git a/services/_template_dot_net/src/YourServiceName.Domain/SeedWork/IRepository.cs b/services/_template_dot_net/src/YourServiceName.Domain/SeedWork/IRepository.cs new file mode 100644 index 00000000..23eb4297 --- /dev/null +++ b/services/_template_dot_net/src/YourServiceName.Domain/SeedWork/IRepository.cs @@ -0,0 +1,12 @@ +namespace YourServiceName.Domain.SeedWork; + +public interface IRepository where T : IAggregateRoot +{ + IUnitOfWork UnitOfWork { get; } +} + +public interface IUnitOfWork : IDisposable +{ + Task SaveChangesAsync(CancellationToken cancellationToken = default); + Task SaveEntitiesAsync(CancellationToken cancellationToken = default); +} diff --git a/services/_template_dot_net/src/YourServiceName.Domain/SeedWork/ValueObject.cs b/services/_template_dot_net/src/YourServiceName.Domain/SeedWork/ValueObject.cs new file mode 100644 index 00000000..d28ec246 --- /dev/null +++ b/services/_template_dot_net/src/YourServiceName.Domain/SeedWork/ValueObject.cs @@ -0,0 +1,40 @@ +namespace YourServiceName.Domain.SeedWork; + +public abstract class ValueObject +{ + protected static bool EqualOperator(ValueObject left, ValueObject right) + { + if (ReferenceEquals(left, null) ^ ReferenceEquals(right, null)) + { + return false; + } + return ReferenceEquals(left, null) || left.Equals(right); + } + + protected static bool NotEqualOperator(ValueObject left, ValueObject right) + { + return !(EqualOperator(left, right)); + } + + protected abstract IEnumerable GetEqualityComponents(); + + public override bool Equals(object obj) + { + if (obj == null || obj.GetType() != GetType()) + { + return false; + } + + var other = (ValueObject)obj; + + return this.GetEqualityComponents().SequenceEqual(other.GetEqualityComponents()); + } + + public override int GetHashCode() + { + return GetEqualityComponents() + .Select(x => x != null ? x.GetHashCode() : 0) + .Aggregate((x, y) => x ^ y); + } + // Other utility methods +} diff --git a/services/_template_dot_net/src/YourServiceName.Domain/YourServiceName.Domain.csproj b/services/_template_dot_net/src/YourServiceName.Domain/YourServiceName.Domain.csproj new file mode 100644 index 00000000..16bfa85d --- /dev/null +++ b/services/_template_dot_net/src/YourServiceName.Domain/YourServiceName.Domain.csproj @@ -0,0 +1,10 @@ + + + + net10.0 + enable + enable + YourServiceName.Domain + + + diff --git a/services/_template_dot_net/src/YourServiceName.Infrastructure/Data/.gitkeep b/services/_template_dot_net/src/YourServiceName.Infrastructure/Data/.gitkeep new file mode 100644 index 00000000..d63f1fea --- /dev/null +++ b/services/_template_dot_net/src/YourServiceName.Infrastructure/Data/.gitkeep @@ -0,0 +1,2 @@ +# EN: .gitkeep file to maintain empty directory structure +# VI: File .gitkeep để duy trì cấu trúc thư mục rỗng diff --git a/services/_template_dot_net/src/YourServiceName.Infrastructure/Data/Interceptors/AuditableEntityInterceptor.cs b/services/_template_dot_net/src/YourServiceName.Infrastructure/Data/Interceptors/AuditableEntityInterceptor.cs new file mode 100644 index 00000000..d5344d1b --- /dev/null +++ b/services/_template_dot_net/src/YourServiceName.Infrastructure/Data/Interceptors/AuditableEntityInterceptor.cs @@ -0,0 +1,40 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Diagnostics; +using YourServiceName.Domain.Common.Interfaces; + +namespace YourServiceName.Infrastructure.Data.Interceptors; + +public class AuditableEntityInterceptor : SaveChangesInterceptor +{ + public override InterceptionResult SavingChanges(DbContextEventData eventData, InterceptionResult result) + { + UpdateEntities(eventData.Context); + return base.SavingChanges(eventData, result); + } + + public override ValueTask> SavingChangesAsync(DbContextEventData eventData, InterceptionResult result, CancellationToken cancellationToken = default) + { + UpdateEntities(eventData.Context); + return base.SavingChangesAsync(eventData, result, cancellationToken); + } + + private void UpdateEntities(DbContext? context) + { + if (context == null) return; + + foreach (var entry in context.ChangeTracker.Entries()) + { + if (entry.State == EntityState.Added) + { + entry.Entity.CreatedAt = DateTime.UtcNow; + entry.Entity.CreatedBy = "system"; // TODO: Get from ICurrentUserService + } + + if (entry.State == EntityState.Added || entry.State == EntityState.Modified) + { + entry.Entity.LastModifiedAt = DateTime.UtcNow; + entry.Entity.LastModifiedBy = "system"; // TODO: Get from ICurrentUserService + } + } + } +} diff --git a/services/_template_dot_net/src/YourServiceName.Infrastructure/DependencyInjection.cs b/services/_template_dot_net/src/YourServiceName.Infrastructure/DependencyInjection.cs new file mode 100644 index 00000000..08ed5362 --- /dev/null +++ b/services/_template_dot_net/src/YourServiceName.Infrastructure/DependencyInjection.cs @@ -0,0 +1,35 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.EntityFrameworkCore; +using YourServiceName.Infrastructure.Data.Interceptors; +using YourServiceName.Domain.SeedWork; + +namespace YourServiceName.Infrastructure; + +public static class DependencyInjection +{ + public static IServiceCollection AddInfrastructure(this IServiceCollection services, IConfiguration configuration) + { + services.AddScoped(); + + services.AddDbContext((sp, options) => + { + var interceptor = sp.GetService(); + + options.UseNpgsql(configuration.GetConnectionString("DefaultConnection"), builder => + { + // EN: Enable retry on failure for Neon Database/Cloud resilience + // VI: Bật tính năng tự động thử lại khi lỗi kết nối (tốt cho Neon/Cloud DB) + builder.EnableRetryOnFailure( + maxRetryCount: 5, + maxRetryDelay: TimeSpan.FromSeconds(30), + errorCodesToAdd: null); + }) + .AddInterceptors(interceptor); + }); + + services.AddScoped(provider => provider.GetRequiredService()); + + return services; + } +} diff --git a/services/_template_dot_net/src/YourServiceName.Infrastructure/Repositories/Repository.cs b/services/_template_dot_net/src/YourServiceName.Infrastructure/Repositories/Repository.cs new file mode 100644 index 00000000..ae0218d6 --- /dev/null +++ b/services/_template_dot_net/src/YourServiceName.Infrastructure/Repositories/Repository.cs @@ -0,0 +1,24 @@ +using Microsoft.EntityFrameworkCore; +using YourServiceName.Domain.SeedWork; + +namespace YourServiceName.Infrastructure.Repositories; + +public abstract class Repository : IRepository where T : Entity, IAggregateRoot +{ + protected readonly YourServiceNameContext _context; + + public Repository(YourServiceNameContext context) + { + _context = context ?? throw new ArgumentNullException(nameof(context)); + } + + public IUnitOfWork UnitOfWork + { + get + { + return _context; + } + } + + // Common repository methods can be added here +} diff --git a/services/_template_dot_net/src/YourServiceName.Infrastructure/Resilience/ResilienceExtensions.cs b/services/_template_dot_net/src/YourServiceName.Infrastructure/Resilience/ResilienceExtensions.cs new file mode 100644 index 00000000..71a56201 --- /dev/null +++ b/services/_template_dot_net/src/YourServiceName.Infrastructure/Resilience/ResilienceExtensions.cs @@ -0,0 +1,32 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Http.Resilience; +using Polly; + +namespace YourServiceName.Infrastructure.Resilience; + +public static class ResilienceExtensions +{ + public static IHttpClientBuilder AddStandardResilienceHandler(this IHttpClientBuilder builder) + { + return builder.AddResilienceHandler("standard-pipeline", builder => + { + // Refer: https://devblogs.microsoft.com/dotnet/building-resilient-web-applications-with-dotnet-circuit-breaker/ + builder.AddRetry(new HttpRetryStrategyOptions + { + MaxRetryAttempts = 3, + Delay = TimeSpan.FromSeconds(2), + BackoffType = DelayBackoffType.Exponential + }); + + builder.AddCircuitBreaker(new HttpCircuitBreakerStrategyOptions + { + SamplingDuration = TimeSpan.FromSeconds(10), + FailureRatio = 0.2, + MinimumThroughput = 3, + BreakDuration = TimeSpan.FromSeconds(30) + }); + + builder.AddTimeout(TimeSpan.FromSeconds(30)); + }); + } +} diff --git a/services/_template_dot_net/src/YourServiceName.Infrastructure/YourServiceName.Infrastructure.csproj b/services/_template_dot_net/src/YourServiceName.Infrastructure/YourServiceName.Infrastructure.csproj new file mode 100644 index 00000000..69b43732 --- /dev/null +++ b/services/_template_dot_net/src/YourServiceName.Infrastructure/YourServiceName.Infrastructure.csproj @@ -0,0 +1,25 @@ + + + + net10.0 + enable + enable + YourServiceName.Infrastructure + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + diff --git a/services/_template_dot_net/src/YourServiceName.Infrastructure/YourServiceNameContext.cs b/services/_template_dot_net/src/YourServiceName.Infrastructure/YourServiceNameContext.cs new file mode 100644 index 00000000..da3121e5 --- /dev/null +++ b/services/_template_dot_net/src/YourServiceName.Infrastructure/YourServiceNameContext.cs @@ -0,0 +1,62 @@ +using MediatR; +using Microsoft.EntityFrameworkCore; +using YourServiceName.Domain.SeedWork; + +namespace YourServiceName.Infrastructure; + +public class YourServiceNameContext : DbContext, IUnitOfWork +{ + private readonly IMediator _mediator; + + public YourServiceNameContext(DbContextOptions options) : base(options) { } + + public YourServiceNameContext(DbContextOptions options, IMediator mediator) : base(options) + { + _mediator = mediator ?? throw new ArgumentNullException(nameof(mediator)); + } + + // DbSet Orders { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.ApplyConfigurationsFromAssembly(typeof(YourServiceNameContext).Assembly); + } + + public async Task SaveEntitiesAsync(CancellationToken cancellationToken = default) + { + // Dispatch Domain Events collection. + // Choices: + // A) Right BEFORE committing data (EF SaveChanges) into the DB will make a single transaction including + // side effects from the domain event handlers which are using the same DbContext with "InstancePerLifetimeScope" or "scoped" lifetime + // B) Right AFTER committing data (EF SaveChanges) into the DB will make multiple transactions. + // You will need to handle eventual consistency and compensatory actions in case of failures in any of the Handlers. + + await _mediator.DispatchDomainEventsAsync(this); + + // After executing this line all the changes (from the Command Handler and Domain Event Handlers) + // performed through the DbContext will be committed + var result = await base.SaveChangesAsync(cancellationToken); + + return true; + } +} + +static class MediatorExtension +{ + public static async Task DispatchDomainEventsAsync(this IMediator mediator, YourServiceNameContext ctx) + { + var domainEntities = ctx.ChangeTracker + .Entries() + .Where(x => x.Entity.DomainEvents != null && x.Entity.DomainEvents.Any()); + + var domainEvents = domainEntities + .SelectMany(x => x.Entity.DomainEvents) + .ToList(); + + domainEntities.ToList() + .ForEach(entity => entity.Entity.ClearDomainEvents()); + + foreach (var domainEvent in domainEvents) + await mediator.Publish(domainEvent); + } +}