using Elastic.Clients.Elasticsearch; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Diagnostics.HealthChecks; using Prometheus; using Serilog; using StackExchange.Redis; using System.Text.Json.Serialization; using FluentValidation; using FluentValidation.AspNetCore; using Microsoft.AspNetCore.Mvc; using System.Reflection; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.IdentityModel.Tokens; using Microsoft.AspNetCore.Authorization; using System.Text; using Microsoft.OpenApi; using Microsoft.AspNetCore.DataProtection; Log.Logger = new LoggerConfiguration() .WriteTo.Console() .CreateBootstrapLogger(); try { var builder = WebApplication.CreateBuilder(args); builder.Services.Configure(builder.Configuration.GetSection(JwtOptions.Section)); var jwtOptions = builder.Configuration.GetSection(JwtOptions.Section).Get()!; if (string.IsNullOrWhiteSpace(jwtOptions.SigningKey) || Encoding.UTF8.GetByteCount(jwtOptions.SigningKey) < 32) throw new InvalidOperationException( "Jwt:SigningKey must be configured and at least 256 bits (32 bytes) for HMAC-SHA256."); builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true, ValidateAudience = true, ValidateLifetime = true, ValidateIssuerSigningKey = true, ValidIssuer = jwtOptions.Issuer, ValidAudience = jwtOptions.Audience, IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtOptions.SigningKey)) }; }); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddAuthorization(options => { options.FallbackPolicy = new AuthorizationPolicyBuilder() .RequireAuthenticatedUser() .Build(); }); builder.Services.AddFluentValidationAutoValidation(); builder.Services.AddValidatorsFromAssemblyContaining(); // Serilog's reloadable logger can only be frozen once per process; skip in // integration tests where WebApplicationFactory may build multiple hosts. if (!builder.Environment.IsEnvironment("Testing")) { builder.Host.UseSerilog((ctx, services, config) => config.ReadFrom.Configuration(ctx.Configuration) .ReadFrom.Services(services) .Enrich.FromLogContext() .Enrich.WithMachineName() .Enrich.WithThreadId()); } builder.Services.AddDbContext((sp, opts) => { opts.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection")); }); builder.Services.AddSingleton(sp => ConnectionMultiplexer.Connect(sp.GetRequiredService()["Redis:ConnectionString"]!)); builder.Services.Configure( builder.Configuration.GetSection(KafkaOptions.Section)); builder.Services.Configure( builder.Configuration.GetSection(ElasticsearchOptions.Section)); var esOptions = builder.Configuration .GetSection(ElasticsearchOptions.Section) .Get()!; builder.Services.AddSingleton( new ElasticsearchClient(new Uri(esOptions.Uri))); builder.Services.Configure( builder.Configuration.GetSection(RabbitMqOptions.Section)); builder.Services.Configure( builder.Configuration.GetSection(MinioOptions.Section)); builder.Services.AddSingleton(); builder.Services.AddHostedService(sp => sp.GetRequiredService()); builder.Services.Configure( builder.Configuration.GetSection(ReconciliationJobOptions.Section)); builder.Services.Configure( builder.Configuration.GetSection(DataLakeOptions.Section)); builder.Services.Configure( builder.Configuration.GetSection(TrendDetectionOptions.SectionName)); builder.Services.Configure( builder.Configuration.GetSection(SuppressionOptions.SectionName)); builder.Services.Configure( builder.Configuration.GetSection(MedicationCorrelationOptions.SectionName)); builder.Services.Configure( builder.Configuration.GetSection(DashboardOptions.Section)); builder.Services.Configure(builder.Configuration.GetSection("Sofa")); var dashboardOptions = builder.Configuration .GetSection(DashboardOptions.Section) .Get() ?? new DashboardOptions(); builder.Services.Configure( builder.Configuration.GetSection(FhirOptions.Section)); builder.Services.Configure( builder.Configuration.GetSection(PatientOptions.Section)); builder.Services.AddDataProtection() .PersistKeysToFileSystem(new DirectoryInfo( builder.Configuration["DataProtection:KeyPath"] ?? "./data-protection-keys")) .SetApplicationName("VigilCareClinical"); builder.Services.Configure( builder.Configuration.GetSection(PhiEncryptionOptions.Section)); builder.Services.AddCors(options => { options.AddPolicy("Dashboard", policy => { policy.WithOrigins(dashboardOptions.CorsOrigins) .AllowAnyHeader() .AllowAnyMethod(); }); }); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddSingleton(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddHttpContextAccessor(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddSingleton(); builder.Services.AddScoped(); builder.Services.AddHostedService(); builder.Services.AddHostedService(); builder.Services.AddHostedService(); builder.Services.AddHostedService(); builder.Services.AddHostedService(); builder.Services.AddHostedService(); builder.Services.AddHostedService(); builder.Services.AddHostedService(); builder.Services.AddHostedService(); builder.Services.AddHostedService(); builder.Services.AddHostedService(); builder.Services.AddSingleton(); builder.Services.AddHostedService(); builder.Services.AddHostedService(); builder.Services.AddHostedService(); builder.Services.AddHostedService(); builder.Services.AddHostedService(); builder.Services.AddHostedService(); builder.Services.AddHostedService(); builder.Services.AddHostedService(); builder.Services.AddHostedService(); builder.Services.AddHostedService(); builder.Services.AddHostedService(); builder.Services.AddHealthChecks() .AddDbContextCheck("postgresql", tags: new[] { "ready" }) .AddCheck("redis", tags: new[] { "ready" }) .AddCheck("kafka", tags: new[] { "ready" }) .AddCheck("rabbitmq", tags: new[] { "ready" }) .AddCheck("elasticsearch", tags: new[] { "ready" }); builder.Services.AddControllers() .AddJsonOptions(opts => { opts.JsonSerializerOptions.ReferenceHandler = ReferenceHandler.IgnoreCycles; opts.JsonSerializerOptions.Converters.Add(new BloodTypeJsonConverter()); opts.JsonSerializerOptions.Converters.Add(new AuditActionJsonConverter()); opts.JsonSerializerOptions.Converters.Add(new SepsisBundleComplianceStatusJsonConverter()); opts.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); opts.JsonSerializerOptions.Converters.Add(new ObservationSourceJsonConverter()); opts.JsonSerializerOptions.Converters.Add(new DepartmentJsonConverter()); }); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(options => { options.SwaggerDoc("v1", new OpenApiInfo { Title = "VigilCare Clinical API", Version = "v1", Description = "Clinical monitoring and alerting platform API" }); options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme { Name = "Authorization", Type = SecuritySchemeType.Http, Scheme = "bearer", BearerFormat = "JWT", In = ParameterLocation.Header, Description = "Enter your JWT token" }); options.AddSecurityRequirement(document => new OpenApiSecurityRequirement { { new OpenApiSecuritySchemeReference("Bearer", document), new List() } }); var xmlPath = Path.Combine(AppContext.BaseDirectory, $"{Assembly.GetExecutingAssembly().GetName().Name}.xml"); options.IncludeXmlComments(xmlPath); }); builder.Services.Configure(options => { options.InvalidModelStateResponseFactory = context => { var errors = context.ModelState .Where(e => e.Value?.Errors.Count > 0) .SelectMany(e => e.Value!.Errors.Select(err => new { field = e.Key, message = err.ErrorMessage })) .ToList(); var response = ApiResponse.Fail(400, "One or more validation errors occurred.", "VALIDATION_ERROR"); return new BadRequestObjectResult(new { response.Success, response.StatusCode, data = (object?)null, error = new { message = "One or more validation errors occurred.", code = "VALIDATION_ERROR", details = errors } }); }; }); var app = builder.Build(); if (!app.Environment.IsEnvironment("Testing")) { using var scope = app.Services.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); var redis = scope.ServiceProvider.GetRequiredService(); await DataSeeder.SeedAsync(db, redis); await UserSeeder.SeedAsync(db); } if (!app.Environment.IsEnvironment("Testing")) { app.UseSerilogRequestLogging(options => { options.MessageTemplate = "HTTP {RequestMethod} {RequestPath} responded {StatusCode} in {Elapsed:0.000}ms"; }); } app.UseSwagger(); app.UseSwaggerUI(options => { options.SwaggerEndpoint("/swagger/v1/swagger.json", "VigilCare Clinical API v1"); }); app.UseMiddleware(); app.UseMiddleware(); app.UseMiddleware(); app.UseCors("Dashboard"); app.UseAuthentication(); app.UseAuthorization(); app.MapHealthChecks("/health/live", new Microsoft.AspNetCore.Diagnostics.HealthChecks.HealthCheckOptions { Predicate = _ => false, ResponseWriter = HealthCheckResponseWriter.WriteAsync }).AllowAnonymous(); app.MapHealthChecks("/health/ready", new Microsoft.AspNetCore.Diagnostics.HealthChecks.HealthCheckOptions { Predicate = check => check.Tags.Contains("ready"), ResponseWriter = HealthCheckResponseWriter.WriteAsync }).AllowAnonymous(); app.MapMetrics("/metrics"); app.MapControllers(); if (args.Contains("encrypt-phi")) { await EncryptPhiCommand.RunAsync(app.Services); return; } app.Run(); } /*** dotnet ef tools use HostFactoryResolver which throws HostAbortedException internally as a control-flow mechanism to stop the host after discovering the DbContext. Your generic catch was swallowing it instead of letting it propagate, so EF saw the process exit abnormally. ***/ catch (HostAbortedException) { throw; } catch (Exception ex) { Log.Fatal(ex, "Application failed to start."); throw; } finally { Log.CloseAndFlush(); } public partial class Program { }