Files
vigilcare-clinical/VigilCareClinicalAPI/Program.cs
T

347 lines
14 KiB
C#

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;
Log.Logger = new LoggerConfiguration()
.WriteTo.Console()
.CreateBootstrapLogger();
try
{
var builder = WebApplication.CreateBuilder(args);
builder.Services.Configure<JwtOptions>(builder.Configuration.GetSection(JwtOptions.Section));
var jwtOptions = builder.Configuration.GetSection(JwtOptions.Section).Get<JwtOptions>()!;
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<IAuthorizationPolicyProvider, PermissionPolicyProvider>();
builder.Services.AddSingleton<IAuthorizationHandler, PermissionAuthorizationHandler>();
builder.Services.AddAuthorization(options =>
{
options.FallbackPolicy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
});
builder.Services.AddFluentValidationAutoValidation();
builder.Services.AddValidatorsFromAssemblyContaining<Program>();
// 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<AppDbContext>(opts =>
opts.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection")));
builder.Services.AddSingleton<IConnectionMultiplexer>(sp =>
ConnectionMultiplexer.Connect(sp.GetRequiredService<IConfiguration>()["Redis:ConnectionString"]!));
builder.Services.Configure<KafkaOptions>(
builder.Configuration.GetSection(KafkaOptions.Section));
builder.Services.Configure<ElasticsearchOptions>(
builder.Configuration.GetSection(ElasticsearchOptions.Section));
var esOptions = builder.Configuration
.GetSection(ElasticsearchOptions.Section)
.Get<ElasticsearchOptions>()!;
builder.Services.AddSingleton(
new ElasticsearchClient(new Uri(esOptions.Uri)));
builder.Services.Configure<RabbitMqOptions>(
builder.Configuration.GetSection(RabbitMqOptions.Section));
builder.Services.Configure<MinioOptions>(
builder.Configuration.GetSection(MinioOptions.Section));
builder.Services.AddSingleton<RabbitMqTopologyProvisioner>();
builder.Services.AddHostedService(sp => sp.GetRequiredService<RabbitMqTopologyProvisioner>());
builder.Services.Configure<ReconciliationJobOptions>(
builder.Configuration.GetSection(ReconciliationJobOptions.Section));
builder.Services.Configure<DataLakeOptions>(
builder.Configuration.GetSection(DataLakeOptions.Section));
builder.Services.Configure<TrendDetectionOptions>(
builder.Configuration.GetSection(TrendDetectionOptions.SectionName));
builder.Services.Configure<SuppressionOptions>(
builder.Configuration.GetSection(SuppressionOptions.SectionName));
builder.Services.Configure<MedicationCorrelationOptions>(
builder.Configuration.GetSection(MedicationCorrelationOptions.SectionName));
builder.Services.Configure<DashboardOptions>(
builder.Configuration.GetSection(DashboardOptions.Section));
builder.Services.Configure<SofaOptions>(builder.Configuration.GetSection("Sofa"));
var dashboardOptions = builder.Configuration
.GetSection(DashboardOptions.Section)
.Get<DashboardOptions>() ?? new DashboardOptions();
builder.Services.Configure<FhirOptions>(
builder.Configuration.GetSection(FhirOptions.Section));
builder.Services.Configure<PatientOptions>(
builder.Configuration.GetSection(PatientOptions.Section));
builder.Services.AddCors(options =>
{
options.AddPolicy("Dashboard", policy =>
{
policy.WithOrigins(dashboardOptions.CorsOrigins)
.AllowAnyHeader()
.AllowAnyMethod();
});
});
builder.Services.AddScoped<IPatientService, PatientService>();
builder.Services.AddScoped<IEncounterService, EncounterService>();
builder.Services.AddScoped<IAlertThresholdService, AlertThresholdService>();
builder.Services.AddScoped<IObservationService, ObservationService>();
builder.Services.AddScoped<IObservationQueryService, ObservationQueryService>();
builder.Services.AddScoped<IAlertService, AlertService>();
builder.Services.AddScoped<IOrderService, OrderService>();
builder.Services.AddScoped<IAnalyticsService, AnalyticsService>();
builder.Services.AddScoped<INews2Service, News2Service>();
builder.Services.AddScoped<IQsofaService, QsofaService>();
builder.Services.AddScoped<ISepsisBundleService, SepsisBundleService>();
builder.Services.AddScoped<SepsisAlertHandler>();
builder.Services.AddScoped<QsofaDetector>();
builder.Services.AddScoped<UnacknowledgedAlertsCheck>();
builder.Services.AddScoped<PendingOrdersCheck>();
builder.Services.AddScoped<DisconnectedMonitorsCheck>();
builder.Services.AddScoped<ReconciliationPublisher>();
builder.Services.AddScoped<WarningEvaluator>();
builder.Services.AddScoped<News2Detector>();
builder.Services.AddScoped<TrendDetector>();
builder.Services.AddSingleton<IAlertSuppressionService, AlertSuppressionService>();
builder.Services.AddScoped<IMedicationService, MedicationService>();
builder.Services.AddScoped<MedicationCorrelationHelper>();
builder.Services.AddScoped<GcsDetector>();
builder.Services.AddScoped<IGcsService, GcsService>();
builder.Services.AddScoped<SofaLabCache>();
builder.Services.AddScoped<SofaVasopressorResolver>();
builder.Services.AddScoped<SofaDetector>();
builder.Services.AddScoped<ISofaService, SofaService>();
builder.Services.AddScoped<IExternalIdentifierService, ExternalIdentifierService>();
builder.Services.AddScoped<FhirReferenceResolver>();
builder.Services.AddScoped<PatientFhirMapper>();
builder.Services.AddScoped<EncounterFhirMapper>();
builder.Services.AddScoped<ObservationFhirMapper>();
builder.Services.AddScoped<MedicationAdministrationFhirMapper>();
builder.Services.AddScoped<FhirBundleProcessor>();
builder.Services.AddScoped<FhirExceptionFilter>();
builder.Services.AddHttpContextAccessor();
builder.Services.AddScoped<ICurrentUserService, CurrentUserService>();
builder.Services.AddScoped<IAuthService, AuthService>();
builder.Services.AddScoped<IAuditService, AuditService>();
builder.Services.AddHostedService<ThresholdCacheLoader>();
builder.Services.AddHostedService<KafkaTopicProvisioner>();
builder.Services.AddHostedService<OutboxRelayService>();
builder.Services.AddHostedService<ElasticIndexProvisioner>();
builder.Services.AddHostedService<EsIndexerService>();
builder.Services.AddHostedService<SepsisEngineService>();
builder.Services.AddHostedService<NotificationPublisherService>();
builder.Services.AddHostedService<PagingWorkerService>();
builder.Services.AddHostedService<EscalationWorkerService>();
builder.Services.AddHostedService<DischargeSummaryWorkerService>();
builder.Services.AddHostedService<ReconciliationScheduler>();
builder.Services.AddSingleton<ClinicalMetrics>();
builder.Services.AddHostedService<AlertsUnacknowledgedCollector>();
builder.Services.AddHostedService<OutboxPendingCollector>();
builder.Services.AddHostedService<KafkaConsumerLagCollector>();
builder.Services.AddHostedService<DataLakeWriterService>();
builder.Services.AddHostedService<WarningAlertService>();
builder.Services.AddHostedService<News2ScoringService>();
builder.Services.AddHostedService<TrendAnalyzerService>();
builder.Services.AddHostedService<SepsisBundleMonitorService>();
builder.Services.AddHostedService<GcsScoringService>();
builder.Services.AddHostedService<SofaScoringService>();
builder.Services.AddHealthChecks()
.AddDbContextCheck<AppDbContext>("postgresql", tags: new[] { "ready" })
.AddCheck<RedisHealthCheck>("redis", tags: new[] { "ready" })
.AddCheck<KafkaHealthCheck>("kafka", tags: new[] { "ready" })
.AddCheck<RabbitMqHealthCheck>("rabbitmq", tags: new[] { "ready" })
.AddCheck<ElasticsearchHealthCheck>("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 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<string>()
}
});
var xmlPath = Path.Combine(AppContext.BaseDirectory,
$"{Assembly.GetExecutingAssembly().GetName().Name}.xml");
options.IncludeXmlComments(xmlPath);
});
builder.Services.Configure<ApiBehaviorOptions>(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<object>.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<AppDbContext>();
var redis = scope.ServiceProvider.GetRequiredService<IConnectionMultiplexer>();
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<CorrelationIdMiddleware>();
app.UseMiddleware<FhirApiKeyOrJwtMiddleware>();
app.UseMiddleware<ExceptionHandlerMiddleware>();
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();
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 { }