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.
This commit is contained in:
Ho Ngoc Hai
2026-01-12 19:12:07 +07:00
parent fdcc24bdf4
commit 616bd9ede9
2 changed files with 190 additions and 205 deletions

View File

@@ -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<Program>();
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<Program>();
// 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<Program>();
cfg.AddOpenBehavior(typeof(LoggingBehavior<,>));
cfg.AddOpenBehavior(typeof(ValidatorBehavior<,>));
cfg.AddOpenBehavior(typeof(TransactionBehavior<,>));
});
// EN: Add FluentValidation / VI: Thêm FluentValidation
builder.Services.AddValidatorsFromAssemblyContaining<Program>();
// 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<string, string>
{
["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<string>()
}
});
// 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<string, string>
{
["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<string>()
}
});
// 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<ILogger<Program>>();
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 { }

View File

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