From 616bd9ede984d8e0034c376b5ad483cb9049c5ec Mon Sep 17 00:00:00 2001 From: Ho Ngoc Hai Date: Mon, 12 Jan 2026 19:12:07 +0700 Subject: [PATCH] feat(api): Refactor Program.cs for improved service configuration and logging - Reorganized the Program.cs file to streamline service configuration, including Serilog setup, API versioning, and health checks. - Added logging configuration to set a minimum logging level for tests, reducing output noise. - Enhanced Swagger integration with detailed API documentation and OAuth2 security definitions. - Implemented ProblemDetails middleware for better error handling and added support for health check endpoints. --- .../src/IamService.API/Program.cs | 387 ++++++++---------- .../CustomWebApplicationFactory.cs | 8 + 2 files changed, 190 insertions(+), 205 deletions(-) diff --git a/services/iam-service-net/src/IamService.API/Program.cs b/services/iam-service-net/src/IamService.API/Program.cs index 2a3e2261..d9513a94 100644 --- a/services/iam-service-net/src/IamService.API/Program.cs +++ b/services/iam-service-net/src/IamService.API/Program.cs @@ -5,240 +5,217 @@ using IamService.API.Application.Behaviors; using IamService.Infrastructure; using Serilog; -// EN: Configure Serilog early / VI: Cấu hình Serilog sớm -Log.Logger = new LoggerConfiguration() - .WriteTo.Console() - .CreateBootstrapLogger(); +var builder = WebApplication.CreateBuilder(args); -try +// EN: Configure Serilog with fresh logger for each host (compatible with WebApplicationFactory) +// VI: Cấu hình Serilog với logger mới cho mỗi host (tương thích với WebApplicationFactory) +builder.Logging.ClearProviders(); +builder.Host.UseSerilog((context, services, configuration) => configuration + .ReadFrom.Configuration(context.Configuration) + .ReadFrom.Services(services) + .Enrich.FromLogContext() + .WriteTo.Console()); + +// EN: Add Infrastructure services (Identity, OpenIddict, Repositories) +// VI: Thêm Infrastructure services (Identity, OpenIddict, Repositories) +builder.Services.AddInfrastructure(builder.Configuration, builder.Environment.EnvironmentName); + +// EN: Add MediatR with behaviors / VI: Thêm MediatR với behaviors +builder.Services.AddMediatR(cfg => { - Log.Information("Starting IAM Service API / Khởi động IAM Service API"); + cfg.RegisterServicesFromAssemblyContaining(); + cfg.AddOpenBehavior(typeof(LoggingBehavior<,>)); + cfg.AddOpenBehavior(typeof(ValidatorBehavior<,>)); + cfg.AddOpenBehavior(typeof(TransactionBehavior<,>)); +}); - var builder = WebApplication.CreateBuilder(args); +// EN: Add FluentValidation / VI: Thêm FluentValidation +builder.Services.AddValidatorsFromAssemblyContaining(); - // EN: Configure Serilog / VI: Cấu hình Serilog - builder.Host.UseSerilog((context, services, configuration) => configuration - .ReadFrom.Configuration(context.Configuration) - .ReadFrom.Services(services) - .Enrich.FromLogContext() - .WriteTo.Console()); +// EN: Add API versioning / VI: Thêm API versioning +builder.Services.AddApiVersioning(options => +{ + options.DefaultApiVersion = new ApiVersion(1, 0); + options.AssumeDefaultVersionWhenUnspecified = true; + options.ReportApiVersions = true; + options.ApiVersionReader = ApiVersionReader.Combine( + new UrlSegmentApiVersionReader(), + new HeaderApiVersionReader("X-Api-Version")); +}) +.AddApiExplorer(options => +{ + options.GroupNameFormat = "'v'VVV"; + options.SubstituteApiVersionInUrl = true; +}); - // EN: Add Infrastructure services (Identity, OpenIddict, Repositories) - // VI: Thêm Infrastructure services (Identity, OpenIddict, Repositories) - builder.Services.AddInfrastructure(builder.Configuration, builder.Environment.EnvironmentName); +// EN: Add controllers / VI: Thêm controllers +builder.Services.AddControllers(); - // EN: Add MediatR with behaviors / VI: Thêm MediatR với behaviors - builder.Services.AddMediatR(cfg => +// EN: Add ProblemDetails middleware (RFC 7807) / VI: Thêm ProblemDetails middleware +builder.Services.AddProblemDetails(options => +{ + options.IncludeExceptionDetails = (ctx, ex) => + builder.Environment.IsDevelopment(); +}); + +// EN: Add Swagger / VI: Thêm Swagger +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(options => +{ + options.SwaggerDoc("v1", new() { - cfg.RegisterServicesFromAssemblyContaining(); - cfg.AddOpenBehavior(typeof(LoggingBehavior<,>)); - cfg.AddOpenBehavior(typeof(ValidatorBehavior<,>)); - cfg.AddOpenBehavior(typeof(TransactionBehavior<,>)); - }); - - // EN: Add FluentValidation / VI: Thêm FluentValidation - builder.Services.AddValidatorsFromAssemblyContaining(); - - // EN: Add API versioning / VI: Thêm API versioning - builder.Services.AddApiVersioning(options => - { - options.DefaultApiVersion = new ApiVersion(1, 0); - options.AssumeDefaultVersionWhenUnspecified = true; - options.ReportApiVersions = true; - options.ApiVersionReader = ApiVersionReader.Combine( - new UrlSegmentApiVersionReader(), - new HeaderApiVersionReader("X-Api-Version")); - }) - .AddApiExplorer(options => - { - options.GroupNameFormat = "'v'VVV"; - options.SubstituteApiVersionInUrl = true; - }); - - // EN: Add controllers / VI: Thêm controllers - builder.Services.AddControllers(); - - // EN: Add ProblemDetails middleware (RFC 7807) / VI: Thêm ProblemDetails middleware - builder.Services.AddProblemDetails(options => - { - options.IncludeExceptionDetails = (ctx, ex) => - builder.Environment.IsDevelopment(); - }); - - // EN: Add Swagger / VI: Thêm Swagger - builder.Services.AddEndpointsApiExplorer(); - builder.Services.AddSwaggerGen(options => - { - options.SwaggerDoc("v1", new() + Title = "IAM Service API", + Version = "v1", + Description = """ + Identity and Access Management Service - OAuth2/OIDC API + + ## Authentication + This API uses OAuth2 with Password Grant and JWT Bearer tokens. + + ## Endpoints + - **/api/v1/auth/register** - Register a new user + - **/connect/token** - OAuth2 token endpoint + - **/api/v1/users** - User management (requires authentication) + """, + Contact = new() { - Title = "IAM Service API", - Version = "v1", - Description = """ - Identity and Access Management Service - OAuth2/OIDC API - - ## Authentication - This API uses OAuth2 with Password Grant and JWT Bearer tokens. - - ## Endpoints - - **/api/v1/auth/register** - Register a new user - - **/connect/token** - OAuth2 token endpoint - - **/api/v1/users** - User management (requires authentication) - """, - Contact = new() - { - Name = "GoodGo Team", - Email = "support@goodgo.com", - Url = new Uri("https://github.com/goodgo") - }, - License = new() - { - Name = "MIT License", - Url = new Uri("https://opensource.org/licenses/MIT") - } - }); - - // EN: Include XML comments for better documentation - // VI: Include XML comments để documentation tốt hơn - var xmlFilename = $"{System.Reflection.Assembly.GetExecutingAssembly().GetName().Name}.xml"; - var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFilename); - if (File.Exists(xmlPath)) + Name = "GoodGo Team", + Email = "support@goodgo.com", + Url = new Uri("https://github.com/goodgo") + }, + License = new() { - options.IncludeXmlComments(xmlPath, includeControllerXmlComments: true); + Name = "MIT License", + Url = new Uri("https://opensource.org/licenses/MIT") } - - // EN: Add OAuth2 security definition / VI: Thêm OAuth2 security definition - options.AddSecurityDefinition("oauth2", new() - { - Type = Microsoft.OpenApi.Models.SecuritySchemeType.OAuth2, - Description = "OAuth2 Password Grant flow. Use email as username.", - Flows = new() - { - Password = new() - { - TokenUrl = new Uri("/connect/token", UriKind.Relative), - Scopes = new Dictionary - { - ["openid"] = "OpenID - Required for authentication", - ["profile"] = "Profile - Access to user profile information", - ["email"] = "Email - Access to user email", - ["roles"] = "Roles - Access to user roles", - ["api"] = "API - Full API access" - } - } - } - }); - - // EN: Add JWT Bearer security definition / VI: Thêm JWT Bearer security definition - options.AddSecurityDefinition("Bearer", new() - { - Type = Microsoft.OpenApi.Models.SecuritySchemeType.Http, - Scheme = "bearer", - BearerFormat = "JWT", - Description = "JWT Authorization header using the Bearer scheme. Example: 'Bearer {token}'" - }); - - options.AddSecurityRequirement(new() - { - { - new() { Reference = new() { Type = Microsoft.OpenApi.Models.ReferenceType.SecurityScheme, Id = "oauth2" } }, - ["api"] - }, - { - new() { Reference = new() { Type = Microsoft.OpenApi.Models.ReferenceType.SecurityScheme, Id = "Bearer" } }, - Array.Empty() - } - }); - - // EN: Enable annotations / VI: Bật annotations - options.EnableAnnotations(); }); - // EN: Add health checks / VI: Thêm health checks - builder.Services.AddHealthChecks() - .AddNpgSql( - builder.Configuration.GetConnectionString("DefaultConnection") - ?? builder.Configuration["DATABASE_URL"] - ?? "", - name: "postgresql", - tags: ["db", "postgresql"]); - - // EN: Add CORS / VI: Thêm CORS - builder.Services.AddCors(options => + // EN: Include XML comments for better documentation + // VI: Include XML comments để documentation tốt hơn + var xmlFilename = $"{System.Reflection.Assembly.GetExecutingAssembly().GetName().Name}.xml"; + var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFilename); + if (File.Exists(xmlPath)) { - options.AddDefaultPolicy(policy => - { - policy.AllowAnyOrigin() - .AllowAnyMethod() - .AllowAnyHeader(); - }); - }); - - var app = builder.Build(); - - // EN: Configure middleware pipeline / VI: Cấu hình middleware pipeline - app.UseSerilogRequestLogging(); - app.UseProblemDetails(); - - if (app.Environment.IsDevelopment()) - { - app.UseSwagger(); - app.UseSwaggerUI(c => - { - c.SwaggerEndpoint("/swagger/v1/swagger.json", "IAM Service API v1"); - c.RoutePrefix = "swagger"; - c.OAuthClientId("swagger-ui"); - c.OAuthUsePkce(); - }); + options.IncludeXmlComments(xmlPath, includeControllerXmlComments: true); } - app.UseCors(); - app.UseRouting(); - - // EN: Debug middleware for /connect/* endpoints - // VI: Debug middleware cho /connect/* endpoints - app.Use(async (context, next) => + // EN: Add OAuth2 security definition / VI: Thêm OAuth2 security definition + options.AddSecurityDefinition("oauth2", new() { - if (context.Request.Path.StartsWithSegments("/connect")) + Type = Microsoft.OpenApi.Models.SecuritySchemeType.OAuth2, + Description = "OAuth2 Password Grant flow. Use email as username.", + Flows = new() { - Log.Information(">>> [DEBUG] Request to {Path} - Method: {Method}", - context.Request.Path, context.Request.Method); - } - await next(); - if (context.Request.Path.StartsWithSegments("/connect")) - { - Log.Information("<<< [DEBUG] Response from {Path} - Status: {StatusCode}", - context.Request.Path, context.Response.StatusCode); + Password = new() + { + TokenUrl = new Uri("/connect/token", UriKind.Relative), + Scopes = new Dictionary + { + ["openid"] = "OpenID - Required for authentication", + ["profile"] = "Profile - Access to user profile information", + ["email"] = "Email - Access to user email", + ["roles"] = "Roles - Access to user roles", + ["api"] = "API - Full API access" + } + } } }); - // EN: Authentication and Authorization / VI: Xác thực và phân quyền - app.UseAuthentication(); - app.UseAuthorization(); - - // EN: Map health check endpoints / VI: Map health check endpoints - app.MapHealthChecks("/health"); - app.MapHealthChecks("/health/live", new() + // EN: Add JWT Bearer security definition / VI: Thêm JWT Bearer security definition + options.AddSecurityDefinition("Bearer", new() { - Predicate = _ => false // EN: Just checks app is running / VI: Chỉ kiểm tra app đang chạy + Type = Microsoft.OpenApi.Models.SecuritySchemeType.Http, + Scheme = "bearer", + BearerFormat = "JWT", + Description = "JWT Authorization header using the Bearer scheme. Example: 'Bearer {token}'" }); - app.MapHealthChecks("/health/ready"); - // EN: Map controllers / VI: Map controllers - app.MapControllers(); + options.AddSecurityRequirement(new() + { + { + new() { Reference = new() { Type = Microsoft.OpenApi.Models.ReferenceType.SecurityScheme, Id = "oauth2" } }, + ["api"] + }, + { + new() { Reference = new() { Type = Microsoft.OpenApi.Models.ReferenceType.SecurityScheme, Id = "Bearer" } }, + Array.Empty() + } + }); - // EN: Run the application / VI: Chạy ứng dụng - app.Run(); -} -catch (Exception ex) + // EN: Enable annotations / VI: Bật annotations + options.EnableAnnotations(); +}); + +// EN: Add health checks / VI: Thêm health checks +var connectionString = builder.Configuration.GetConnectionString("DefaultConnection") + ?? builder.Configuration["DATABASE_URL"] + ?? ""; + +if (!string.IsNullOrEmpty(connectionString)) { - Log.Fatal(ex, "Application terminated unexpectedly / Ứng dụng kết thúc bất ngờ"); - throw; + builder.Services.AddHealthChecks() + .AddNpgSql(connectionString, name: "postgresql", tags: ["db", "postgresql"]); } -finally +else { - Log.CloseAndFlush(); + builder.Services.AddHealthChecks(); } +// EN: Add CORS / VI: Thêm CORS +builder.Services.AddCors(options => +{ + options.AddDefaultPolicy(policy => + { + policy.AllowAnyOrigin() + .AllowAnyMethod() + .AllowAnyHeader(); + }); +}); + +var app = builder.Build(); + +// EN: Log startup message using ILogger +// VI: Log thông báo khởi động sử dụng ILogger +var logger = app.Services.GetRequiredService>(); +logger.LogInformation("Starting IAM Service API / Khởi động IAM Service API"); + +// EN: Configure middleware pipeline / VI: Cấu hình middleware pipeline +app.UseSerilogRequestLogging(); +app.UseProblemDetails(); + +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(c => + { + c.SwaggerEndpoint("/swagger/v1/swagger.json", "IAM Service API v1"); + c.RoutePrefix = "swagger"; + c.OAuthClientId("swagger-ui"); + c.OAuthUsePkce(); + }); +} + +app.UseCors(); +app.UseRouting(); + +// EN: Authentication and Authorization / VI: Xác thực và phân quyền +app.UseAuthentication(); +app.UseAuthorization(); + +// EN: Map health check endpoints / VI: Map health check endpoints +app.MapHealthChecks("/health"); +app.MapHealthChecks("/health/live", new() +{ + Predicate = _ => false // EN: Just checks app is running / VI: Chỉ kiểm tra app đang chạy +}); +app.MapHealthChecks("/health/ready"); + +// EN: Map controllers / VI: Map controllers +app.MapControllers(); + +// EN: Run the application / VI: Chạy ứng dụng +app.Run(); + // EN: Make Program class accessible for integration tests // VI: Làm cho class Program có thể truy cập cho integration tests public partial class Program { } diff --git a/services/iam-service-net/tests/IamService.FunctionalTests/CustomWebApplicationFactory.cs b/services/iam-service-net/tests/IamService.FunctionalTests/CustomWebApplicationFactory.cs index 67cd954f..987a1f51 100644 --- a/services/iam-service-net/tests/IamService.FunctionalTests/CustomWebApplicationFactory.cs +++ b/services/iam-service-net/tests/IamService.FunctionalTests/CustomWebApplicationFactory.cs @@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Logging; using IamService.Infrastructure; using IamService.Infrastructure.Caching; using StackExchange.Redis; @@ -66,6 +67,13 @@ public class CustomWebApplicationFactory : WebApplicationFactory options.UseOpenIddict(); // EN: Required for OpenIddict / VI: Cần cho OpenIddict options.EnableSensitiveDataLogging(); }); + + // EN: Set minimum logging level to reduce test output noise + // VI: Đặt mức logging tối thiểu để giảm nhiễu output test + services.AddLogging(logging => + { + logging.SetMinimumLevel(LogLevel.Warning); + }); }); }