feat: Integrate YARP Reverse Proxy for API routing, removing local API controllers, and updating project configuration and documentation.

This commit is contained in:
Ho Ngoc Hai
2026-01-19 09:30:52 +07:00
parent 3b41106606
commit e1dfc4e7a7
6 changed files with 143 additions and 259 deletions

View File

@@ -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));
}
}

View File

@@ -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));
}
}

View File

@@ -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

View File

@@ -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>

View 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"
}
}
}
}
}
}