518 lines
22 KiB
C#
518 lines
22 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;
|
|
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<JwtOptions>(builder.Configuration.GetSection(JwtOptions.Section));
|
|
|
|
var jwtOptions = builder.Configuration.GetSection(JwtOptions.Section).Get<JwtOptions>()!;
|
|
|
|
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))
|
|
};
|
|
})
|
|
.AddScheme<AuthenticationSchemeOptions, GatewayApiKeyAuthenticationHandler>(
|
|
GatewayApiKeyAuthenticationHandler.SchemeName, null);
|
|
|
|
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>((sp, 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(_ =>
|
|
{
|
|
var settings = new ElasticsearchClientSettings(new Uri(esOptions.Uri));
|
|
|
|
if (!string.IsNullOrWhiteSpace(esOptions.ApiKey))
|
|
settings = settings.Authentication(new Elastic.Transport.ApiKey(esOptions.ApiKey));
|
|
else if (!string.IsNullOrWhiteSpace(esOptions.Username))
|
|
settings = settings.Authentication(
|
|
new Elastic.Transport.BasicAuthentication(esOptions.Username, esOptions.Password ?? ""));
|
|
|
|
if (esOptions.DisableCertificateValidation)
|
|
settings = settings.ServerCertificateValidationCallback((_, _, _, _) => true);
|
|
|
|
return new ElasticsearchClient(settings);
|
|
});
|
|
|
|
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<ClinicalSyncOptions>(
|
|
builder.Configuration.GetSection(ClinicalSyncOptions.Section));
|
|
|
|
builder.Services.Configure<FhirOptions>(
|
|
builder.Configuration.GetSection(FhirOptions.Section));
|
|
|
|
builder.Services.Configure<PatientOptions>(
|
|
builder.Configuration.GetSection(PatientOptions.Section));
|
|
|
|
builder.Services.AddDataProtection()
|
|
.PersistKeysToFileSystem(new DirectoryInfo(
|
|
builder.Configuration["DataProtection:KeyPath"] ?? "./data-protection-keys"))
|
|
.SetApplicationName("VigilCareClinical");
|
|
|
|
builder.Services.Configure<PhiEncryptionOptions>(
|
|
builder.Configuration.GetSection(PhiEncryptionOptions.Section));
|
|
|
|
builder.Services.Configure<ClinicalSyncOptions>(builder.Configuration.GetSection(ClinicalSyncOptions.Section));
|
|
|
|
builder.Services.Configure<GatewayMonitoringOptions>(
|
|
builder.Configuration.GetSection(GatewayMonitoringOptions.Section));
|
|
|
|
builder.Services.Configure<AlertQualityOptions>(
|
|
builder.Configuration.GetSection(AlertQualityOptions.Section));
|
|
|
|
builder.Services.Configure<SimulationOptions>(
|
|
builder.Configuration.GetSection(SimulationOptions.Section));
|
|
|
|
builder.Services.Configure<SeedingOptions>(
|
|
builder.Configuration.GetSection(SeedingOptions.Section));
|
|
|
|
var simulationOptions = builder.Configuration
|
|
.GetSection(SimulationOptions.Section).Get<SimulationOptions>() ?? new();
|
|
|
|
var seedingOptions = builder.Configuration.GetSection(SeedingOptions.Section).Get<SeedingOptions>()
|
|
?? new SeedingOptions();
|
|
|
|
if (simulationOptions.Enabled)
|
|
{
|
|
builder.Services.AddHttpClient("simulation-loopback", c =>
|
|
c.BaseAddress = new Uri(simulationOptions.LoopbackBaseUrl));
|
|
builder.Services.AddSingleton<ISimulationClientFactory, SimulationClientFactory>();
|
|
builder.Services.AddSingleton<IScenarioCatalog, ScenarioCatalog>();
|
|
builder.Services.AddSingleton<ISessionCatalog, SessionCatalog>();
|
|
builder.Services.AddSingleton<SimulationRunner>();
|
|
builder.Services.AddSingleton<ISimulationRunner>(sp => sp.GetRequiredService<SimulationRunner>());
|
|
builder.Services.AddHostedService(sp => sp.GetRequiredService<SimulationRunner>());
|
|
builder.Services.AddScoped<ISimulationPurgeService, SimulationPurgeService>();
|
|
}
|
|
|
|
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<IDischargeSummaryService, DischargeSummaryService>();
|
|
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<IUserService, UserService>();
|
|
builder.Services.AddScoped<IAuditService, AuditService>();
|
|
builder.Services.AddScoped<IClinicalSyncService, ClinicalSyncService>();
|
|
builder.Services.AddScoped<ClinicalSyncBatchProcessor>();
|
|
builder.Services.AddSingleton<IPhiEncryptionService, PhiEncryptionService>();
|
|
builder.Services.AddScoped<IPhiAccessLogService, PhiAccessLogService>();
|
|
builder.Services.AddScoped<ISiteService, SiteService>();
|
|
builder.Services.AddScoped<IGatewayRegistryService, GatewayRegistryService>();
|
|
builder.Services.AddScoped<IOperationsService, OperationsService>();
|
|
builder.Services.AddHostedService<GatewayStaleDetectorService>();
|
|
builder.Services.AddScoped<IAlertQualityMetricsService, AlertQualityMetricsService>();
|
|
|
|
builder.Services.AddHostedService<ThresholdCacheLoader>();
|
|
builder.Services.AddHostedService<KafkaTopicProvisioner>();
|
|
builder.Services.AddHostedService<OutboxRelayService>();
|
|
builder.Services.AddHostedService<ClinicalSyncBatchConsumer>();
|
|
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.AddHostedService<PatientPhiMigrationService>();
|
|
builder.Services.AddHostedService<WardGatewayMetricsCollector>();
|
|
builder.Services.AddHostedService<AlertQualityAggregatorService>();
|
|
|
|
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 AuditActionJsonConverter());
|
|
opts.JsonSerializerOptions.Converters.Add(new SepsisBundleComplianceStatusJsonConverter());
|
|
opts.JsonSerializerOptions.Converters.Add(new AlertTypeJsonConverter());
|
|
opts.JsonSerializerOptions.Converters.Add(new AlertSeverityJsonConverter());
|
|
opts.JsonSerializerOptions.Converters.Add(new AlertStatusJsonConverter());
|
|
opts.JsonSerializerOptions.Converters.Add(new ObservationSourceJsonConverter());
|
|
opts.JsonSerializerOptions.Converters.Add(new DepartmentJsonConverter());
|
|
opts.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
|
|
});
|
|
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();
|
|
|
|
// One-shot CLI verbs exit before hosting. Skip seeding for them so
|
|
// create-admin / register-gateway never race auto-bootstrap.
|
|
var isCliCommand = args.Contains("encrypt-phi")
|
|
|| args.Contains("create-admin")
|
|
|| args.Contains("register-gateway");
|
|
|
|
if (simulationOptions.Enabled && !isCliCommand)
|
|
{
|
|
Log.Warning(
|
|
"Simulation mode ENABLED — scenario replay endpoints are exposed. " +
|
|
"Do not run this configuration against real patient data.");
|
|
}
|
|
|
|
// Demo data — including the seeded demo users with well-known passwords —
|
|
// must never be created in production. Seeding:EnableDemoData defaults to
|
|
// true so local development and the existing verification scripts are
|
|
// unaffected; appsettings.Production.json sets it to false.
|
|
var enableDemoData = seedingOptions.EnableDemoData
|
|
&& !app.Environment.IsEnvironment("Testing")
|
|
&& !isCliCommand;
|
|
|
|
var runBootstrap = !app.Environment.IsEnvironment("Testing") && !isCliCommand;
|
|
|
|
if (runBootstrap)
|
|
{
|
|
using var scope = app.Services.CreateScope();
|
|
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
|
var redis = scope.ServiceProvider.GetRequiredService<IConnectionMultiplexer>();
|
|
var bootstrapLogger = scope.ServiceProvider
|
|
.GetRequiredService<ILoggerFactory>()
|
|
.CreateLogger("BootstrapSeeder");
|
|
// Always: default thresholds (+ optional bootstrap users / site+gateway from config).
|
|
await BootstrapSeeder.SeedAsync(db, redis, seedingOptions, bootstrapLogger);
|
|
}
|
|
|
|
if (enableDemoData)
|
|
{
|
|
using var scope = app.Services.CreateScope();
|
|
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
|
var redis = scope.ServiceProvider.GetRequiredService<IConnectionMultiplexer>();
|
|
await DataSeeder.SeedAsync(db, redis);
|
|
await GatewayRegistrySeeder.SeedAsync(db);
|
|
await UserSeeder.SeedAsync(
|
|
db,
|
|
simulationEnabled: simulationOptions.Enabled,
|
|
simulationRunnerPassword: simulationOptions.RunnerPassword);
|
|
}
|
|
else if (simulationOptions.Enabled && !isCliCommand && !app.Environment.IsEnvironment("Testing"))
|
|
{
|
|
using var scope = app.Services.CreateScope();
|
|
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
|
await UserSeeder.EnsureSimulationRunnerAsync(db, simulationOptions.RunnerPassword);
|
|
}
|
|
|
|
if (!app.Environment.IsEnvironment("Testing"))
|
|
{
|
|
app.UseSerilogRequestLogging(options =>
|
|
{
|
|
options.MessageTemplate =
|
|
"HTTP {RequestMethod} {RequestPath} responded {StatusCode} in {Elapsed:0.000}ms";
|
|
});
|
|
}
|
|
|
|
if (builder.Configuration.GetValue("Swagger:Enabled", !app.Environment.IsProduction()))
|
|
{
|
|
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").AllowAnonymous();
|
|
app.MapControllers();
|
|
|
|
if (args.Contains("encrypt-phi"))
|
|
{
|
|
await EncryptPhiCommand.RunAsync(app.Services);
|
|
return;
|
|
}
|
|
|
|
if (args.Contains("create-admin"))
|
|
{
|
|
try
|
|
{
|
|
Environment.ExitCode = await CreateAdminCommand.RunAsync(app.Services, args);
|
|
}
|
|
catch (ArgumentException ex)
|
|
{
|
|
Console.Error.WriteLine(ex.Message);
|
|
Environment.ExitCode = 1;
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (args.Contains("register-gateway"))
|
|
{
|
|
try
|
|
{
|
|
Environment.ExitCode = await RegisterGatewayCommand.RunAsync(app.Services, args);
|
|
}
|
|
catch (ArgumentException ex)
|
|
{
|
|
Console.Error.WriteLine(ex.Message);
|
|
Environment.ExitCode = 1;
|
|
}
|
|
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 { } |