diff --git a/GermanApp/GermanApp.csproj b/GermanApp/GermanApp.csproj index da9d6b3..e71a865 100644 --- a/GermanApp/GermanApp.csproj +++ b/GermanApp/GermanApp.csproj @@ -19,6 +19,10 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive + + + + diff --git a/GermanApp/Program.cs b/GermanApp/Program.cs index d672186..014fb9c 100644 --- a/GermanApp/Program.cs +++ b/GermanApp/Program.cs @@ -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(options => +try { - // 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()) + var builder = WebApplication.CreateBuilder(args); + + // 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(); + + // 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(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(); + + // ============================================ + // APPLICATION LAYER - Use Cases & Services + // ============================================ + + // Register command handlers + builder.Services.AddScoped, 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(); + // Use CORS + app.UseCors("AllowAll"); -// ============================================ -// APPLICATION LAYER - Use Cases & Services -// ============================================ + // Use Health Checks + app.MapHealthChecks("/health"); -// Register command handlers -builder.Services.AddScoped, 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 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 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"); -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) diff --git a/GermanApp/Shared/Middleware/ExceptionMiddleware.cs b/GermanApp/Shared/Middleware/ExceptionMiddleware.cs new file mode 100644 index 0000000..bc25881 --- /dev/null +++ b/GermanApp/Shared/Middleware/ExceptionMiddleware.cs @@ -0,0 +1,62 @@ +using System.Net; +using System.Text.Json; +using GermanApp.Shared.Models; + +namespace GermanApp.Shared.Middleware; + +/// +/// Global exception handling middleware. +/// Catches exceptions and returns consistent error responses. +/// +public class ExceptionMiddleware +{ + private readonly RequestDelegate _next; + private readonly ILogger _logger; + + public ExceptionMiddleware(RequestDelegate next, ILogger 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)); + } +} + +/// +/// Extension method to add exception middleware. +/// +public static class ExceptionMiddlewareExtensions +{ + public static IApplicationBuilder UseExceptionMiddleware(this IApplicationBuilder builder) => + builder.UseMiddleware(); +} diff --git a/GermanApp/Shared/Models/ApiResponse.cs b/GermanApp/Shared/Models/ApiResponse.cs new file mode 100644 index 0000000..a496c52 --- /dev/null +++ b/GermanApp/Shared/Models/ApiResponse.cs @@ -0,0 +1,47 @@ +namespace GermanApp.Shared.Models; + +/// +/// Base API response wrapper for consistent API responses. +/// +/// The data type of the response +public record ApiResponse(T Data, string Message = "Success", bool Success = true) +{ + /// + /// Creates a successful response. + /// + public static ApiResponse Ok(T data, string message = "Success") => + new ApiResponse(data, message, true); +} + +/// +/// API error response for consistent error handling. +/// +public record ApiErrorResponse(string Message, int StatusCode, string? Details = null) +{ + public bool Success => false; + + /// + /// Creates an error response. + /// + public static ApiErrorResponse Error(string message, int statusCode, string? details = null) => + new ApiErrorResponse(message, statusCode, details); +} + +/// +/// Paginated API response. +/// +/// The data type of the items +public record PaginatedResponse(IEnumerable Items, int PageNumber, int PageSize, int TotalCount, int TotalPages) +{ + /// + /// Creates a paginated response. + /// + public static PaginatedResponse Create(IEnumerable items, int pageNumber, int pageSize, int totalCount) => + new PaginatedResponse( + items, + pageNumber, + pageSize, + totalCount, + (int)Math.Ceiling(totalCount / (double)pageSize) + ); +}