feat: Integrate YARP Reverse Proxy for API routing, removing local API controllers, and updating project configuration and documentation.
This commit is contained in:
@@ -1,22 +1,45 @@
|
||||
# WebClientBase - Blazor Web App .NET 10
|
||||
|
||||
Base frontend web application cho GoodGo Platform được xây dựng với Blazor WebAssembly Hosted.
|
||||
Base frontend web application cho GoodGo Platform được xây dựng với Blazor WebAssembly + BFF Pattern.
|
||||
|
||||
## Features / Tính năng
|
||||
## Architecture / Kiến trúc
|
||||
|
||||
- **Blazor WebAssembly Hosted** - Client chạy trong browser, Server host API
|
||||
- **Shared Library Pattern** - DTOs và Validation được chia sẻ giữa Client và Server
|
||||
- **Data Annotations Validation** - Viết 1 lần, dùng chung cả 2 đầu
|
||||
- **[ApiController] Auto-Validation** - Server tự động validate, không cần `if (!ModelState.IsValid)`
|
||||
- **Dark/Light Mode** - CSS variables với theme toggle
|
||||
- **Glassmorphism UI** - Modern design với backdrop blur và shadows
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ Browser │
|
||||
│ ┌─────────────────────────────────────────────────────────────┐ │
|
||||
│ │ Blazor WebAssembly Client │ │
|
||||
│ └─────────────────────────────────────────────────────────────┘ │
|
||||
└────────────────────────────────┬────────────────────────────────────┘
|
||||
│ /api/*
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ BFF (Backend for Frontend) │
|
||||
│ WebClientBase.Server + YARP │
|
||||
│ ┌─────────────────────────────────────────────────────────────┐ │
|
||||
│ │ Routes: /api/iam/** → iam-service │ │
|
||||
│ │ /api/merchants/** → merchant-service │ │
|
||||
│ │ /api/catalog/** → catalog-service │ │
|
||||
│ │ /api/orders/** → order-service │ │
|
||||
│ └─────────────────────────────────────────────────────────────┘ │
|
||||
└────────────────────────────────┬────────────────────────────────────┘
|
||||
│ Internal Network
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ Microservices │
|
||||
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
|
||||
│ │ IAM │ │ Merchant │ │ Catalog │ │ Order │ │
|
||||
│ │ :5101 │ │ :5102 │ │ :5103 │ │ :5104 │ │
|
||||
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
|
||||
└─────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Tech Stack
|
||||
|
||||
| Layer | Technology |
|
||||
|-------|------------|
|
||||
| Client | Blazor WebAssembly (.NET 10) |
|
||||
| Server | ASP.NET Core Web API (.NET 10) |
|
||||
| BFF | ASP.NET Core + YARP Reverse Proxy |
|
||||
| Shared | Class Library với Data Annotations |
|
||||
| Styling | CSS Variables, Dark Mode |
|
||||
|
||||
@@ -28,10 +51,11 @@ web-client-base-net/
|
||||
├── src/
|
||||
│ ├── WebClientBase.Client/ # Blazor WebAssembly
|
||||
│ │ ├── Layout/ # MainLayout, NavMenu
|
||||
│ │ ├── Pages/ # Razor pages (Products, Auth)
|
||||
│ │ ├── Pages/ # Razor pages
|
||||
│ │ └── wwwroot/css/ # Design System CSS
|
||||
│ ├── WebClientBase.Server/ # ASP.NET Core Host
|
||||
│ │ └── Controllers/ # API Controllers
|
||||
│ ├── WebClientBase.Server/ # BFF with YARP Proxy
|
||||
│ │ ├── Program.cs # YARP configuration
|
||||
│ │ └── yarp.json # Routes configuration
|
||||
│ └── WebClientBase.Shared/ # Shared Library
|
||||
│ └── DTOs/ # Data Transfer Objects
|
||||
└── Dockerfile
|
||||
@@ -46,71 +70,27 @@ cd apps/web-client-base-net
|
||||
# Restore packages
|
||||
dotnet restore
|
||||
|
||||
# Run Server (hosts both API and Blazor client)
|
||||
# Run BFF Server (hosts Blazor client + YARP proxy)
|
||||
dotnet run --project src/WebClientBase.Server
|
||||
|
||||
# Open browser
|
||||
# https://localhost:5001 hoặc http://localhost:5000
|
||||
# http://localhost:5091
|
||||
```
|
||||
|
||||
## Shared Validation Pattern / Mẫu Validation Chia Sẻ
|
||||
## BFF Routes Configuration
|
||||
|
||||
### 1. Define DTO with Validation (một lần)
|
||||
```csharp
|
||||
// WebClientBase.Shared/DTOs/ProductDto.cs
|
||||
public class ProductDto
|
||||
{
|
||||
[Required(ErrorMessage = "Tên là bắt buộc / Name is required")]
|
||||
[StringLength(100, MinimumLength = 3)]
|
||||
public string Name { get; set; } = string.Empty;
|
||||
Routes được cấu hình trong `yarp.json`:
|
||||
|
||||
[Range(0.01, 1_000_000)]
|
||||
public decimal Price { get; set; }
|
||||
}
|
||||
```
|
||||
| Client Route | Proxied To | Description |
|
||||
|--------------|------------|-------------|
|
||||
| `/api/iam/**` | `http://localhost:5101/` | Identity & Access Management |
|
||||
| `/api/merchants/**` | `http://localhost:5102/` | Merchant Service |
|
||||
| `/api/catalog/**` | `http://localhost:5103/` | Catalog Service |
|
||||
| `/api/orders/**` | `http://localhost:5104/` | Order Service |
|
||||
|
||||
### 2. Server (Auto-validation với [ApiController])
|
||||
```csharp
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class ProductsController : ControllerBase
|
||||
{
|
||||
[HttpPost]
|
||||
public IActionResult Create(ProductDto request)
|
||||
{
|
||||
// Không cần if (!ModelState.IsValid) - [ApiController] đã làm hộ
|
||||
return Ok(ApiResponse<ProductDto>.Ok(request));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Client (DataAnnotationsValidator)
|
||||
```razor
|
||||
<EditForm Model="@product" OnValidSubmit="HandleSubmit">
|
||||
<DataAnnotationsValidator />
|
||||
<ValidationSummary />
|
||||
|
||||
<InputText @bind-Value="product.Name" />
|
||||
<ValidationMessage For="() => product.Name" />
|
||||
|
||||
<button type="submit">Submit</button>
|
||||
</EditForm>
|
||||
```
|
||||
|
||||
## API Endpoints
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| GET | `/api/products` | Get all products |
|
||||
| POST | `/api/products` | Create product |
|
||||
| PUT | `/api/products` | Update product |
|
||||
| POST | `/api/auth/register` | Register user |
|
||||
| POST | `/api/auth/login` | Login |
|
||||
| GET | `/api/auth/profile/{id}` | Get profile |
|
||||
| GET | `/health` | Health check |
|
||||
> **Note:** Addresses có thể override qua environment variables trong production.
|
||||
|
||||
## Related Skills
|
||||
|
||||
- [React Enterprise Architect](../../.agent/skills/react-enterprise-architect/SKILL.md)
|
||||
- [Tailwind Design System](../../.agent/skills/tailwind-design-system/SKILL.md)
|
||||
- [API Aggregation](../../.agent/skills/api-aggregation/SKILL.md) - BFF Pattern
|
||||
- [Project Rules](../../.agent/skills/project-rules/SKILL.md)
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using WebClientBase.Shared;
|
||||
using WebClientBase.Shared.DTOs;
|
||||
|
||||
namespace WebClientBase.Server.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Authentication API controller.
|
||||
/// VI: Auth API controller.
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class AuthController : ControllerBase
|
||||
{
|
||||
/// <summary>
|
||||
/// EN: Register a new user.
|
||||
/// VI: Đăng ký user mới.
|
||||
/// </summary>
|
||||
/// <param name="request">Registration data with validation</param>
|
||||
[HttpPost("register")]
|
||||
[ProducesResponseType(typeof(ApiResponse<UserProfileDto>), StatusCodes.Status201Created)]
|
||||
[ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status400BadRequest)]
|
||||
public ActionResult<ApiResponse<UserProfileDto>> Register([FromBody] RegisterDto request)
|
||||
{
|
||||
// EN: [ApiController] validates before this code runs
|
||||
// VI: [ApiController] validate trước khi code này chạy
|
||||
|
||||
// EN: Demo - create user profile
|
||||
// VI: Demo - tạo user profile
|
||||
var profile = new UserProfileDto
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Email = request.Email,
|
||||
DisplayName = request.DisplayName,
|
||||
CreatedAt = DateTime.UtcNow
|
||||
};
|
||||
|
||||
return CreatedAtAction(nameof(GetProfile), new { id = profile.Id }, ApiResponse<UserProfileDto>.Ok(profile));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Login with email and password.
|
||||
/// VI: Đăng nhập với email và mật khẩu.
|
||||
/// </summary>
|
||||
[HttpPost("login")]
|
||||
[ProducesResponseType(typeof(ApiResponse<UserProfileDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status400BadRequest)]
|
||||
[ProducesResponseType(typeof(ApiResponse), StatusCodes.Status401Unauthorized)]
|
||||
public ActionResult<ApiResponse<UserProfileDto>> Login([FromBody] LoginDto request)
|
||||
{
|
||||
// EN: Demo - always succeed with mock profile
|
||||
// VI: Demo - luôn thành công với profile giả
|
||||
var profile = new UserProfileDto
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Email = request.Email,
|
||||
DisplayName = "Demo User",
|
||||
CreatedAt = DateTime.UtcNow.AddDays(-30)
|
||||
};
|
||||
|
||||
return Ok(ApiResponse<UserProfileDto>.Ok(profile));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Get user profile by ID.
|
||||
/// VI: Lấy user profile theo ID.
|
||||
/// </summary>
|
||||
[HttpGet("profile/{id:guid}")]
|
||||
[ProducesResponseType(typeof(ApiResponse<UserProfileDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)]
|
||||
public ActionResult<ApiResponse<UserProfileDto>> GetProfile(Guid id)
|
||||
{
|
||||
// EN: Demo - return mock profile
|
||||
// VI: Demo - trả về profile giả
|
||||
var profile = new UserProfileDto
|
||||
{
|
||||
Id = id,
|
||||
Email = "demo@example.com",
|
||||
DisplayName = "Demo User",
|
||||
AvatarUrl = "https://example.com/avatar.jpg",
|
||||
CreatedAt = DateTime.UtcNow.AddDays(-30)
|
||||
};
|
||||
|
||||
return Ok(ApiResponse<UserProfileDto>.Ok(profile));
|
||||
}
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using WebClientBase.Shared;
|
||||
using WebClientBase.Shared.DTOs;
|
||||
|
||||
namespace WebClientBase.Server.Controllers;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Products API controller with automatic ModelState validation.
|
||||
/// VI: Products API controller với validation ModelState tự động.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// EN: The [ApiController] attribute automatically validates ModelState before action executes.
|
||||
/// VI: Attribute [ApiController] tự động validate ModelState trước khi action được thực thi.
|
||||
/// </remarks>
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class ProductsController : ControllerBase
|
||||
{
|
||||
// EN: In-memory storage for demo purposes
|
||||
// VI: Lưu trữ trong bộ nhớ cho mục đích demo
|
||||
private static readonly List<ProductDto> _products = new()
|
||||
{
|
||||
new ProductDto { Name = "MacBook Pro", Description = "Laptop cao cấp", Price = 2499.99m, Quantity = 10 },
|
||||
new ProductDto { Name = "iPhone 15 Pro", Description = "Điện thoại thông minh", Price = 1199.99m, Quantity = 50 }
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// EN: Get all products.
|
||||
/// VI: Lấy tất cả sản phẩm.
|
||||
/// </summary>
|
||||
[HttpGet]
|
||||
[ProducesResponseType(typeof(ApiResponse<List<ProductDto>>), StatusCodes.Status200OK)]
|
||||
public ActionResult<ApiResponse<List<ProductDto>>> GetAll()
|
||||
{
|
||||
return Ok(ApiResponse<List<ProductDto>>.Ok(_products));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Create a new product.
|
||||
/// VI: Tạo sản phẩm mới.
|
||||
/// </summary>
|
||||
/// <param name="request">Product data with validation</param>
|
||||
/// <remarks>
|
||||
/// EN: [ApiController] automatically returns 400 if ModelState is invalid.
|
||||
/// VI: [ApiController] tự động trả về 400 nếu ModelState không hợp lệ.
|
||||
/// No need for: if (!ModelState.IsValid) return BadRequest(ModelState);
|
||||
/// </remarks>
|
||||
[HttpPost]
|
||||
[ProducesResponseType(typeof(ApiResponse<ProductDto>), StatusCodes.Status201Created)]
|
||||
[ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status400BadRequest)]
|
||||
public ActionResult<ApiResponse<ProductDto>> Create([FromBody] CreateProductDto request)
|
||||
{
|
||||
// EN: Validation is automatically done by [ApiController] before reaching here
|
||||
// VI: Validation được tự động thực hiện bởi [ApiController] trước khi đến đây
|
||||
|
||||
var product = new ProductDto
|
||||
{
|
||||
Name = request.Name,
|
||||
Description = request.Description,
|
||||
Price = request.Price,
|
||||
Quantity = request.Quantity
|
||||
};
|
||||
|
||||
_products.Add(product);
|
||||
|
||||
return CreatedAtAction(
|
||||
nameof(GetAll),
|
||||
ApiResponse<ProductDto>.Ok(product)
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// EN: Update an existing product.
|
||||
/// VI: Cập nhật sản phẩm đã có.
|
||||
/// </summary>
|
||||
[HttpPut]
|
||||
[ProducesResponseType(typeof(ApiResponse<ProductDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status400BadRequest)]
|
||||
public ActionResult<ApiResponse<ProductDto>> Update([FromBody] UpdateProductDto request)
|
||||
{
|
||||
// EN: Find and update (simplified for demo)
|
||||
// VI: Tìm và cập nhật (đơn giản hóa cho demo)
|
||||
var existing = _products.FirstOrDefault(p => p.Name == request.Name);
|
||||
if (existing == null)
|
||||
{
|
||||
return NotFound(ApiResponse<ProductDto>.Fail("Sản phẩm không tồn tại / Product not found"));
|
||||
}
|
||||
|
||||
existing.Description = request.Description;
|
||||
existing.Price = request.Price;
|
||||
existing.Quantity = request.Quantity;
|
||||
|
||||
return Ok(ApiResponse<ProductDto>.Ok(existing));
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
/// <summary>
|
||||
/// EN: ASP.NET Core Server with Blazor WebAssembly hosting.
|
||||
/// VI: ASP.NET Core Server với Blazor WebAssembly hosting.
|
||||
/// EN: ASP.NET Core BFF (Backend for Frontend) with YARP Reverse Proxy.
|
||||
/// VI: ASP.NET Core BFF (Backend for Frontend) với YARP Reverse Proxy.
|
||||
/// </summary>
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
@@ -10,9 +10,14 @@ var builder = WebApplication.CreateBuilder(args);
|
||||
// VI: Thêm các services vào container
|
||||
// ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
// EN: Add controllers with automatic ModelState validation via [ApiController]
|
||||
// VI: Thêm controllers với validation ModelState tự động qua [ApiController]
|
||||
builder.Services.AddControllers();
|
||||
// EN: Load YARP configuration from yarp.json
|
||||
// VI: Load cấu hình YARP từ yarp.json
|
||||
builder.Configuration.AddJsonFile("yarp.json", optional: false, reloadOnChange: true);
|
||||
|
||||
// EN: Add YARP Reverse Proxy
|
||||
// VI: Thêm YARP Reverse Proxy
|
||||
builder.Services.AddReverseProxy()
|
||||
.LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"));
|
||||
|
||||
// EN: Add OpenAPI/Swagger support
|
||||
// VI: Thêm hỗ trợ OpenAPI/Swagger
|
||||
@@ -24,7 +29,7 @@ builder.Services.AddCors(options =>
|
||||
{
|
||||
options.AddPolicy("BlazorClient", policy =>
|
||||
{
|
||||
policy.WithOrigins("https://localhost:5001", "http://localhost:5000")
|
||||
policy.AllowAnyOrigin()
|
||||
.AllowAnyMethod()
|
||||
.AllowAnyHeader();
|
||||
});
|
||||
@@ -55,16 +60,15 @@ app.UseCors("BlazorClient");
|
||||
|
||||
// EN: Serve static files with fingerprinting support (.NET 10+)
|
||||
// VI: Phục vụ static files với hỗ trợ fingerprinting (.NET 10+)
|
||||
// IMPORTANT: MapStaticAssets handles fingerprinted file mappings automatically
|
||||
app.MapStaticAssets();
|
||||
|
||||
// EN: Map health check endpoint
|
||||
// VI: Map endpoint health check
|
||||
app.MapHealthChecks("/health");
|
||||
|
||||
// EN: Map API controllers
|
||||
// VI: Map các API controllers
|
||||
app.MapControllers();
|
||||
// EN: Map YARP Reverse Proxy routes to microservices
|
||||
// VI: Map các routes YARP Reverse Proxy đến microservices
|
||||
app.MapReverseProxy();
|
||||
|
||||
// EN: Fallback to index.html for SPA routing
|
||||
// VI: Fallback đến index.html cho SPA routing
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.1" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="10.0.1" />
|
||||
<PackageReference Include="Yarp.ReverseProxy" Version="2.3.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
80
apps/web-client-base-net/src/WebClientBase.Server/yarp.json
Normal file
80
apps/web-client-base-net/src/WebClientBase.Server/yarp.json
Normal file
@@ -0,0 +1,80 @@
|
||||
{
|
||||
"ReverseProxy": {
|
||||
"Routes": {
|
||||
"iam-route": {
|
||||
"ClusterId": "iam-cluster",
|
||||
"Match": {
|
||||
"Path": "/api/iam/{**catch-all}"
|
||||
},
|
||||
"Transforms": [
|
||||
{
|
||||
"PathRemovePrefix": "/api/iam"
|
||||
}
|
||||
]
|
||||
},
|
||||
"merchant-route": {
|
||||
"ClusterId": "merchant-cluster",
|
||||
"Match": {
|
||||
"Path": "/api/merchants/{**catch-all}"
|
||||
},
|
||||
"Transforms": [
|
||||
{
|
||||
"PathRemovePrefix": "/api/merchants"
|
||||
}
|
||||
]
|
||||
},
|
||||
"catalog-route": {
|
||||
"ClusterId": "catalog-cluster",
|
||||
"Match": {
|
||||
"Path": "/api/catalog/{**catch-all}"
|
||||
},
|
||||
"Transforms": [
|
||||
{
|
||||
"PathRemovePrefix": "/api/catalog"
|
||||
}
|
||||
]
|
||||
},
|
||||
"order-route": {
|
||||
"ClusterId": "order-cluster",
|
||||
"Match": {
|
||||
"Path": "/api/orders/{**catch-all}"
|
||||
},
|
||||
"Transforms": [
|
||||
{
|
||||
"PathRemovePrefix": "/api/orders"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"Clusters": {
|
||||
"iam-cluster": {
|
||||
"Destinations": {
|
||||
"destination1": {
|
||||
"Address": "http://localhost:5101"
|
||||
}
|
||||
}
|
||||
},
|
||||
"merchant-cluster": {
|
||||
"Destinations": {
|
||||
"destination1": {
|
||||
"Address": "http://localhost:5102"
|
||||
}
|
||||
}
|
||||
},
|
||||
"catalog-cluster": {
|
||||
"Destinations": {
|
||||
"destination1": {
|
||||
"Address": "http://localhost:5103"
|
||||
}
|
||||
}
|
||||
},
|
||||
"order-cluster": {
|
||||
"Destinations": {
|
||||
"destination1": {
|
||||
"Address": "http://localhost:5104"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user