258 lines
11 KiB
C#
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 { } |