Files
vigilcare-clinical/VigilCareClinicalAPI/Program.cs
T

258 lines
11 KiB
C#

using Elastic.Clients.Elasticsearch;
using Microsoft.EntityFrameworkCore;
using Prometheus;
using Serilog;
using StackExchange.Redis;
using System.Text.Json.Serialization;
using FluentValidation;
using FluentValidation.AspNetCore;
using Microsoft.AspNetCore.Mvc;
using System.Reflection;
Log.Logger = new LoggerConfiguration()
.WriteTo.Console()
.CreateBootstrapLogger();
try
{
var builder = WebApplication.CreateBuilder(args);
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.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.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.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 =>
{
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);
}
if (!app.Environment.IsEnvironment("Testing"))
{
app.UseSerilogRequestLogging(options =>
{
options.MessageTemplate =
"HTTP {RequestMethod} {RequestPath} responded {StatusCode} in {Elapsed:0.000}ms";
});
}
app.UseMiddleware<CorrelationIdMiddleware>();
app.UseMiddleware<FhirApiKeyMiddleware>();
app.UseMiddleware<ExceptionHandlerMiddleware>();
app.UseCors("Dashboard");
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
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 { }