using Adapters.Driven.Persistence; using Adapters.Driven.Persistence.Context; using Adapters.Driving.Api.Extensions; using Application; using dotenv.net; using Microsoft.AspNetCore.Diagnostics.HealthChecks; using Microsoft.AspNetCore.RateLimiting; using Microsoft.AspNetCore.ResponseCompression; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Diagnostics.HealthChecks; using Serilog; using System.IO.Compression; using System.Text.Json; using System.Threading.RateLimiting; // Load .env file (searches up to root directory) DotEnv.Load(options: new DotEnvOptions( probeForEnv: true, probeLevelsToSearch: 5 )); // Configure Serilog from appsettings + environment variables Log.Logger = new LoggerConfiguration() .ReadFrom.Configuration(new ConfigurationBuilder() .AddJsonFile("appsettings.json") .AddJsonFile($"appsettings.{Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Production"}.json", optional: true) .AddEnvironmentVariables() .Build()) .CreateLogger(); try { Log.Information("Starting application"); var builder = WebApplication.CreateBuilder(args); // Add environment variables to configuration builder.Configuration.AddEnvironmentVariables(); // Build connection string from individual env vars if not provided directly var connectionString = builder.Configuration.GetConnectionString("DefaultConnection"); if (string.IsNullOrEmpty(connectionString)) { var dbHost = Environment.GetEnvironmentVariable("DB_HOST") ?? "localhost"; var dbPort = Environment.GetEnvironmentVariable("DB_PORT") ?? "1433"; var dbName = Environment.GetEnvironmentVariable("DB_NAME") ?? "StudentEnrollment"; var dbUser = Environment.GetEnvironmentVariable("DB_USER") ?? "sa"; var dbPassword = Environment.GetEnvironmentVariable("DB_PASSWORD") ?? ""; connectionString = $"Server={dbHost},{dbPort};Database={dbName};User Id={dbUser};Password={dbPassword};TrustServerCertificate=True"; builder.Configuration["ConnectionStrings:DefaultConnection"] = connectionString; } builder.Host.UseSerilog(); // Add services builder.Services.AddApplication(); builder.Services.AddPersistence(builder.Configuration); builder.Services.AddGraphQLApi(); // Response Compression (Brotli preferred, then Gzip) builder.Services.AddResponseCompression(options => { options.EnableForHttps = true; options.Providers.Add(); options.Providers.Add(); options.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat( ["application/json", "application/graphql-response+json"]); }); builder.Services.Configure(options => options.Level = CompressionLevel.Fastest); builder.Services.Configure(options => options.Level = CompressionLevel.Fastest); // CORS (configurable via CORS_ORIGINS env var, comma-separated) var corsOrigins = Environment.GetEnvironmentVariable("CORS_ORIGINS")?.Split(',', StringSplitOptions.RemoveEmptyEntries) ?? ["http://localhost:4200"]; builder.Services.AddCors(options => { options.AddDefaultPolicy(policy => policy.WithOrigins(corsOrigins) .AllowAnyHeader() .AllowAnyMethod()); }); // Output caching for read-heavy operations builder.Services.AddOutputCache(options => { options.AddBasePolicy(policy => policy.Expire(TimeSpan.FromSeconds(10))); options.AddPolicy("Subjects", policy => policy.Expire(TimeSpan.FromMinutes(5))); options.AddPolicy("Professors", policy => policy.Expire(TimeSpan.FromMinutes(5))); }); // Health checks builder.Services.AddHealthChecks() .AddDbContextCheck("database"); // Rate limiting (DoS prevention) builder.Services.AddRateLimiter(options => { options.RejectionStatusCode = StatusCodes.Status429TooManyRequests; options.AddFixedWindowLimiter("graphql", limiter => { limiter.PermitLimit = 100; limiter.Window = TimeSpan.FromMinutes(1); limiter.QueueProcessingOrder = QueueProcessingOrder.OldestFirst; limiter.QueueLimit = 10; }); options.AddFixedWindowLimiter("mutations", limiter => { limiter.PermitLimit = 30; limiter.Window = TimeSpan.FromMinutes(1); limiter.QueueLimit = 5; }); }); var app = builder.Build(); // Verify database connection and apply migrations await VerifyDatabaseConnectionAsync(app); async Task VerifyDatabaseConnectionAsync(WebApplication app) { var maxRetries = 10; var delay = TimeSpan.FromSeconds(5); for (int attempt = 1; attempt <= maxRetries; attempt++) { try { using var scope = app.Services.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); Log.Information("Attempting database connection (attempt {Attempt}/{MaxRetries})...", attempt, maxRetries); // Try to apply migrations - this will create the DB if it doesn't exist Log.Information("Applying database migrations (this will create DB if needed)..."); await db.Database.MigrateAsync(); Log.Information("Database migrations applied successfully"); // Verify connection works after migrations if (await db.Database.CanConnectAsync()) { Log.Information("Database connection verified successfully"); return; } } catch (Exception ex) { Log.Error(ex, "Database operation failed (attempt {Attempt}/{MaxRetries}): {ErrorMessage}", attempt, maxRetries, ex.Message); if (attempt == maxRetries) { Log.Fatal("Could not establish database connection after {MaxRetries} attempts. " + "Please verify: DB_HOST={DbHost}, DB_PORT={DbPort}, DB_NAME={DbName}, DB_USER={DbUser}", maxRetries, Environment.GetEnvironmentVariable("DB_HOST") ?? "localhost", Environment.GetEnvironmentVariable("DB_PORT") ?? "1433", Environment.GetEnvironmentVariable("DB_NAME") ?? "StudentEnrollment", Environment.GetEnvironmentVariable("DB_USER") ?? "sa"); } } if (attempt < maxRetries) { Log.Information("Waiting {Delay} seconds before next connection attempt...", delay.TotalSeconds); await Task.Delay(delay); } } } // Security headers (OWASP recommended) app.Use(async (context, next) => { var headers = context.Response.Headers; headers.Append("X-Content-Type-Options", "nosniff"); headers.Append("X-Frame-Options", "DENY"); headers.Append("X-XSS-Protection", "0"); headers.Append("Referrer-Policy", "strict-origin-when-cross-origin"); headers.Append("Permissions-Policy", "geolocation=(), microphone=(), camera=()"); // CSP: More restrictive in production, relaxed for dev (GraphQL Playground needs unsafe-inline/eval) var csp = app.Environment.IsDevelopment() ? "default-src 'self'; " + "script-src 'self' 'unsafe-inline' 'unsafe-eval'; " + "style-src 'self' 'unsafe-inline'; " + "img-src 'self' data: https:; " + "font-src 'self'; " + "connect-src 'self' http://localhost:* https://localhost:*; " + "frame-ancestors 'none'; " + "base-uri 'self'; " + "form-action 'self'" : "default-src 'self'; " + "script-src 'self'; " + "style-src 'self'; " + "img-src 'self' data:; " + "font-src 'self'; " + "connect-src 'self'; " + "frame-ancestors 'none'; " + "base-uri 'self'; " + "form-action 'self'"; headers.Append("Content-Security-Policy", csp); if (!context.Request.IsHttps && !app.Environment.IsDevelopment()) { headers.Append("Strict-Transport-Security", "max-age=31536000; includeSubDomains"); } await next(); }); // Request logging (structured logs, excludes sensitive data) app.UseSerilogRequestLogging(options => { options.EnrichDiagnosticContext = (diagnosticContext, httpContext) => { diagnosticContext.Set("RequestHost", httpContext.Request.Host.Value); diagnosticContext.Set("UserAgent", httpContext.Request.Headers.UserAgent.ToString()); }; options.MessageTemplate = "HTTP {RequestMethod} {RequestPath} responded {StatusCode} in {Elapsed:0.0000} ms"; }); // Middleware order matters for performance app.UseResponseCompression(); app.UseCors(); app.UseRateLimiter(); app.UseOutputCache(); // Health check endpoint with JSON response app.MapHealthChecks("/health", new HealthCheckOptions { ResponseWriter = async (context, report) => { context.Response.ContentType = "application/json"; var result = new { status = report.Status.ToString(), timestamp = DateTime.UtcNow, checks = report.Entries.Select(e => new { name = e.Key, status = e.Value.Status.ToString(), description = e.Value.Description, duration = e.Value.Duration.TotalMilliseconds }) }; await context.Response.WriteAsync(JsonSerializer.Serialize(result, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase })); } }); // GraphQL endpoint with Banana Cake Pop playground and rate limiting app.MapGraphQL() .RequireRateLimiting("graphql") .WithOptions(new HotChocolate.AspNetCore.GraphQLServerOptions { Tool = { Enable = app.Environment.IsDevelopment() } }); await app.RunAsync(); } catch (Exception ex) { Log.Fatal(ex, "Application terminated unexpectedly"); } finally { await Log.CloseAndFlushAsync(); }