feat(backend/infra): add health checks, CORS, Serilog logging, and exception middleware
- Add Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore package - Add Serilog.AspNetCore and Serilog.Sinks.Console packages - Configure Serilog with bootstrap logger and configuration - Add Health Checks endpoint at /health with DbContext check - Configure CORS with AllowAll policy for development - Create ApiResponse, ApiErrorResponse, and PaginatedResponse models - Create ExceptionMiddleware for global error handling - Restructure Program.cs with try-catch-finally for proper logging Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
parent
accb06bc7c
commit
4ee2b2568a
4 changed files with 231 additions and 69 deletions
|
|
@ -19,6 +19,10 @@
|
|||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="9.0.0" />
|
||||
<PackageReference Include="Serilog.AspNetCore" Version="8.0.0" />
|
||||
<PackageReference Include="Serilog.Sinks.Console" Version="5.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
|
|||
|
|
@ -5,88 +5,137 @@ using GermanApp.Infrastructure.Data.DbContext;
|
|||
using GermanApp.Infrastructure.Data.Repositories;
|
||||
using GermanApp.Infrastructure.Data.SeedData;
|
||||
using GermanApp.Presentation.Endpoints;
|
||||
using GermanApp.Shared.Middleware;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Serilog;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
// Configure Serilog
|
||||
Log.Logger = new LoggerConfiguration()
|
||||
.MinimumLevel.Debug()
|
||||
.WriteTo.Console()
|
||||
.CreateBootstrapLogger();
|
||||
|
||||
// Add services to the container.
|
||||
// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
|
||||
builder.Services.AddOpenApi();
|
||||
|
||||
// ============================================
|
||||
// INFRASTRUCTURE LAYER - Database & External Services
|
||||
// ============================================
|
||||
|
||||
// Add DbContext with PostgreSQL
|
||||
builder.Services.AddDbContext<AppDbContext>(options =>
|
||||
try
|
||||
{
|
||||
// Using PostgreSQL for production and development
|
||||
options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection")
|
||||
?? "Host=localhost;Port=5432;Database=DeutschLernen;Username=postgres;Password=postgres");
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
// Enable sensitive data logging in development
|
||||
if (builder.Environment.IsDevelopment())
|
||||
// Add Serilog to the builder
|
||||
builder.Host.UseSerilog((ctx, lc) => lc
|
||||
.MinimumLevel.Debug()
|
||||
.WriteTo.Console()
|
||||
.ReadFrom.Configuration(ctx.Configuration));
|
||||
|
||||
// ============================================
|
||||
// PRESENTATION LAYER - API Configuration
|
||||
// ============================================
|
||||
|
||||
// Add services to the container.
|
||||
builder.Services.AddOpenApi();
|
||||
|
||||
// Add Health Checks
|
||||
builder.Services.AddHealthChecks()
|
||||
.AddDbContextCheck<AppDbContext>();
|
||||
|
||||
// Configure CORS
|
||||
builder.Services.AddCors(options =>
|
||||
{
|
||||
options.EnableSensitiveDataLogging();
|
||||
options.EnableDetailedErrors();
|
||||
options.AddPolicy("AllowAll", builder =>
|
||||
{
|
||||
builder.AllowAnyOrigin()
|
||||
.AllowAnyMethod()
|
||||
.AllowAnyHeader();
|
||||
});
|
||||
});
|
||||
|
||||
// Add services for Minimal APIs
|
||||
builder.Services.AddEndpointsApiExplorer();
|
||||
builder.Services.AddSwaggerGen();
|
||||
|
||||
// ============================================
|
||||
// INFRASTRUCTURE LAYER - Database & External Services
|
||||
// ============================================
|
||||
|
||||
// Add DbContext with PostgreSQL
|
||||
builder.Services.AddDbContext<AppDbContext>(options =>
|
||||
{
|
||||
// Using PostgreSQL for production and development
|
||||
options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection")
|
||||
?? "Host=localhost;Port=5432;Database=DeutschLernen;Username=postgres;Password=postgres");
|
||||
|
||||
// Enable sensitive data logging in development
|
||||
if (builder.Environment.IsDevelopment())
|
||||
{
|
||||
options.EnableSensitiveDataLogging();
|
||||
options.EnableDetailedErrors();
|
||||
}
|
||||
});
|
||||
|
||||
// Register repositories (Infrastructure implementations of Domain interfaces)
|
||||
builder.Services.AddScoped<ILessonRepository, LessonRepository>();
|
||||
|
||||
// ============================================
|
||||
// APPLICATION LAYER - Use Cases & Services
|
||||
// ============================================
|
||||
|
||||
// Register command handlers
|
||||
builder.Services.AddScoped<ICommandHandler<CreateLessonCommand, LessonDto>, CreateLessonCommandHandler>();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
// Configure the HTTP request pipeline.
|
||||
|
||||
// Use exception middleware first (to catch all exceptions)
|
||||
app.UseExceptionMiddleware();
|
||||
|
||||
if (app.Environment.IsDevelopment())
|
||||
{
|
||||
app.MapOpenApi();
|
||||
app.UseSwagger();
|
||||
app.UseSwaggerUI();
|
||||
}
|
||||
});
|
||||
|
||||
// Register repositories (Infrastructure implementations of Domain interfaces)
|
||||
builder.Services.AddScoped<ILessonRepository, LessonRepository>();
|
||||
// Use CORS
|
||||
app.UseCors("AllowAll");
|
||||
|
||||
// ============================================
|
||||
// APPLICATION LAYER - Use Cases & Services
|
||||
// ============================================
|
||||
// Use Health Checks
|
||||
app.MapHealthChecks("/health");
|
||||
|
||||
// Register command handlers
|
||||
builder.Services.AddScoped<ICommandHandler<CreateLessonCommand, LessonDto>, CreateLessonCommandHandler>();
|
||||
// Seed database with initial data
|
||||
app.SeedDatabase();
|
||||
|
||||
// ============================================
|
||||
// PRESENTATION LAYER - API
|
||||
// ============================================
|
||||
// Map Clean Architecture endpoints
|
||||
app.MapLessonsEndpoints();
|
||||
|
||||
// Add services for Minimal APIs
|
||||
builder.Services.AddEndpointsApiExplorer();
|
||||
builder.Services.AddSwaggerGen();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
// Configure the HTTP request pipeline.
|
||||
if (app.Environment.IsDevelopment())
|
||||
{
|
||||
app.MapOpenApi();
|
||||
app.UseSwagger();
|
||||
app.UseSwaggerUI();
|
||||
}
|
||||
|
||||
// Seed database with initial data
|
||||
app.SeedDatabase();
|
||||
|
||||
// Map Clean Architecture endpoints
|
||||
app.MapLessonsEndpoints();
|
||||
|
||||
// Keep original WeatherForecast endpoint for reference
|
||||
app.MapGet("/weatherforecast", () =>
|
||||
{
|
||||
var summaries = new[]
|
||||
// Keep original WeatherForecast endpoint for reference
|
||||
app.MapGet("/weatherforecast", () =>
|
||||
{
|
||||
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
|
||||
};
|
||||
var summaries = new[]
|
||||
{
|
||||
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
|
||||
};
|
||||
|
||||
var forecast = Enumerable.Range(1, 5).Select(index =>
|
||||
new WeatherForecast
|
||||
(
|
||||
DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
|
||||
Random.Shared.Next(-20, 55),
|
||||
summaries[Random.Shared.Next(summaries.Length)]
|
||||
))
|
||||
.ToArray();
|
||||
return forecast;
|
||||
})
|
||||
.WithName("GetWeatherForecast");
|
||||
var forecast = Enumerable.Range(1, 5).Select(index =>
|
||||
new WeatherForecast
|
||||
(
|
||||
DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
|
||||
Random.Shared.Next(-20, 55),
|
||||
summaries[Random.Shared.Next(summaries.Length)]
|
||||
))
|
||||
.ToArray();
|
||||
return forecast;
|
||||
})
|
||||
.WithName("GetWeatherForecast");
|
||||
|
||||
app.Run();
|
||||
app.Run();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Fatal(ex, "Application terminated unexpectedly");
|
||||
}
|
||||
finally
|
||||
{
|
||||
Log.CloseAndFlush();
|
||||
}
|
||||
|
||||
// Existing record for reference
|
||||
record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary)
|
||||
|
|
|
|||
62
GermanApp/Shared/Middleware/ExceptionMiddleware.cs
Normal file
62
GermanApp/Shared/Middleware/ExceptionMiddleware.cs
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
using System.Net;
|
||||
using System.Text.Json;
|
||||
using GermanApp.Shared.Models;
|
||||
|
||||
namespace GermanApp.Shared.Middleware;
|
||||
|
||||
/// <summary>
|
||||
/// Global exception handling middleware.
|
||||
/// Catches exceptions and returns consistent error responses.
|
||||
/// </summary>
|
||||
public class ExceptionMiddleware
|
||||
{
|
||||
private readonly RequestDelegate _next;
|
||||
private readonly ILogger<ExceptionMiddleware> _logger;
|
||||
|
||||
public ExceptionMiddleware(RequestDelegate next, ILogger<ExceptionMiddleware> logger)
|
||||
{
|
||||
_next = next;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task InvokeAsync(HttpContext httpContext)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _next(httpContext);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Unhandled exception occurred: {Message}", ex.Message);
|
||||
await HandleExceptionAsync(httpContext, ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static Task HandleExceptionAsync(HttpContext context, Exception exception)
|
||||
{
|
||||
context.Response.ContentType = "application/json";
|
||||
context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
|
||||
|
||||
var response = new ApiErrorResponse(
|
||||
"An unexpected error occurred. Please try again later.",
|
||||
context.Response.StatusCode,
|
||||
exception.Message
|
||||
);
|
||||
|
||||
var options = new JsonSerializerOptions
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
||||
};
|
||||
|
||||
return context.Response.WriteAsync(JsonSerializer.Serialize(response, options));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extension method to add exception middleware.
|
||||
/// </summary>
|
||||
public static class ExceptionMiddlewareExtensions
|
||||
{
|
||||
public static IApplicationBuilder UseExceptionMiddleware(this IApplicationBuilder builder) =>
|
||||
builder.UseMiddleware<ExceptionMiddleware>();
|
||||
}
|
||||
47
GermanApp/Shared/Models/ApiResponse.cs
Normal file
47
GermanApp/Shared/Models/ApiResponse.cs
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
namespace GermanApp.Shared.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Base API response wrapper for consistent API responses.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The data type of the response</typeparam>
|
||||
public record ApiResponse<T>(T Data, string Message = "Success", bool Success = true)
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a successful response.
|
||||
/// </summary>
|
||||
public static ApiResponse<T> Ok(T data, string message = "Success") =>
|
||||
new ApiResponse<T>(data, message, true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// API error response for consistent error handling.
|
||||
/// </summary>
|
||||
public record ApiErrorResponse(string Message, int StatusCode, string? Details = null)
|
||||
{
|
||||
public bool Success => false;
|
||||
|
||||
/// <summary>
|
||||
/// Creates an error response.
|
||||
/// </summary>
|
||||
public static ApiErrorResponse Error(string message, int statusCode, string? details = null) =>
|
||||
new ApiErrorResponse(message, statusCode, details);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Paginated API response.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The data type of the items</typeparam>
|
||||
public record PaginatedResponse<T>(IEnumerable<T> Items, int PageNumber, int PageSize, int TotalCount, int TotalPages)
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a paginated response.
|
||||
/// </summary>
|
||||
public static PaginatedResponse<T> Create(IEnumerable<T> items, int pageNumber, int pageSize, int totalCount) =>
|
||||
new PaginatedResponse<T>(
|
||||
items,
|
||||
pageNumber,
|
||||
pageSize,
|
||||
totalCount,
|
||||
(int)Math.Ceiling(totalCount / (double)pageSize)
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue