using System.Reflection; using System.Text; using GB5Shared.GOP.Qualifier; using FastEndpoints; using FastEndpoints.Swagger; using GB5Shared.Auth.Jwt; using GB5Shared.Authorization; using GB5Shared.Connection; using GB5Shared.Telemetry; using GB5Shared.Vault; using GB5Shared.Deployment; using GB5Shared.DateConverter; using static GB5Shared.DateConverter.GB5JsonOptions; using GB5Shared.DTO.Framework.CommonConfig; using GB5Shared.EntityHandler; using GB5Shared.GB5Library.Qualifier; using GB5Shared.GenerateAutoNumber; using GB5Shared.ListQuery; using GB5Shared.PubSub.OutBox; using GB5Shared.QueryExecutor; using GB5Shared.Storage; using GB5Shared.Validation; using GB5Shared.WorkFlow.WorkFlowEngine; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.HttpOverrides; using Microsoft.AspNetCore.ResponseCompression; using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.Extensions.Caching.Hybrid; using Microsoft.IdentityModel.Tokens; using OpenTelemetry; using VaultSharp; using VaultSharp.V1.AuthMethods.Token; Console.OutputEncoding = Encoding.UTF8; var builder = WebApplication.CreateBuilder(args); // ── Port from config — change "AppPort" in appsettings.json to use any port ── var appPort = builder.Configuration.GetValue("AppPort"); builder.WebHost.UseUrls($"http://0.0.0.0:{appPort}"); builder.Services.AddDaprClient(); builder.Services.AddHttpClient(); builder.Services.AddControllers() .AddDapr() .AddJsonOptions(options => { options.JsonSerializerOptions.PropertyNamingPolicy = null; options.JsonSerializerOptions.DictionaryKeyPolicy = null; options.JsonSerializerOptions.AddGB5Converters(); }); builder.Services.ConfigureHttpJsonOptions(options => { options.SerializerOptions.PropertyNamingPolicy = null; options.SerializerOptions.DictionaryKeyPolicy = null; options.SerializerOptions.AddGB5Converters(); }); #pragma warning disable EXTEXP0018 builder.Services.AddHybridCache(options => { options.DefaultEntryOptions = new HybridCacheEntryOptions(); options.DisableCompression = false; }); #pragma warning restore EXTEXP0018 builder.Services.AddMemoryCache(); builder.Services.AddDistributedMemoryCache(); builder.Services.AddHttpContextAccessor(); builder.Services.AddScoped(); builder.Services.AddScoped(); // QueryExecutor depends on IGB5CommonFunction — missing registration only surfaces at runtime // DI-graph build time (FastEndpoints validates every endpoint's constructor chain on startup), // not at dotnet build; discovered via live verification (2026-07-15). Mirrors PAYSL/ComplianceSL's // identical registration. builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); // VerifyKyc.cs is the one DXP endpoint gated via [MenuRights] instead of the vendor JWT scheme // (a GB-client-side internal-admin action, not vendor self-service) — mirrors Entitlement's // identical registration (Thread 0 §20.1). builder.Services.AddMenuRightsAuthorization(); // Universal Save Execution Pipeline — required for every BLL that calls // BaseEntityAppService.ExecuteSaveAsync (see PERMSL/Program.cs for the identical set). // WorkFlowEngine's own transitive dependencies — same runtime-only DI gap pattern, discovered // via live verification (2026-07-15). Mirrors PERMSL/Program.cs's full set. builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(typeof(BaseEntityAppService<>), typeof(BaseEntityAppService<>)); // QualifierFacade's full transitive dependency set — same runtime-only DI gap as // IGB5CommonFunction above, discovered via live verification (2026-07-15). Mirrors PAYSL/Program.cs // exactly (DXP's own code never references these by name, same as QualifierEngine/IGB5CommonFunction — // they're internal to BaseEntityAppService's save pipeline). builder.Services.AddScoped(); builder.Services.AddGB5QualifierEngine(); builder.Services.AddScoped(); // Vault + shared JWT issuer — consolidated onto GB5Shared.Vault/GB5Shared.Auth.Jwt instead of a // module-local IVaultClient registration. AddGB5Vault registers IVaultClient/IVaultService (with // real caching/retry, unlike the old hand-rolled block this replaces); AddGB5JwtIssuer registers // the shared HMAC-SHA256 issuance mechanics DXPJwtService now delegates to (previously // duplicated byte-for-byte in EntitlementBLL.Auth.ClientJwtService). builder.Services.AddGB5Vault(builder.Configuration); builder.Services.AddGB5JwtIssuer(); // Deployment tier (Singleton — GB5:Environment=Dev|QC|Live) — DXP's system DB connection // name (DXPDb) resolves through IGB5Environment.Resolve() so Dev/QC/Live each hit a // separate physical database (see DXPSystemContext). builder.Services.AddGB5Environment(builder.Configuration); builder.Services.Configure( builder.Configuration.GetSection(DXPBLL.Options.DXPJwtOptions.SectionName)); // DXPSecretResolver's one remaining responsibility (KYC field encryption) is unrelated to JWT and // still module-local — see its own doc comment. builder.Services.AddScoped(); // JWT bearer — DXP's own signed session layer, distinct from the internal LoginDTO trust model // (see Part 2 of the DXP plan). The signing key is resolved from Vault once at startup (a // justified exception to "no sync-over-async" — this runs once before the host starts serving // requests, not inside the request pipeline) and reused for both token validation here and // token issuance in DXPJwtService (which fetches the same key via IDXPSecretResolver, cached). var dxpVaultAuthMethod = new TokenAuthMethodInfo(builder.Configuration["Vault:Token"]!); var dxpVaultSettings = new VaultSharp.VaultClientSettings(builder.Configuration["Vault:Address"]!, dxpVaultAuthMethod); var dxpVaultClient = new VaultSharp.VaultClient(dxpVaultSettings); var dxpJwtSigningKey = dxpVaultClient.V1.Secrets.KeyValue.V2 .ReadSecretAsync(path: "dxp/jwt-signing-key", mountPoint: "secret") .GetAwaiter().GetResult() .Data.Data.Values.First().ToString()!; var dxpJwtIssuer = builder.Configuration["DXPJwt:Issuer"]!; var dxpJwtAudience = builder.Configuration["DXPJwt:Audience"]!; builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true, ValidIssuer = dxpJwtIssuer, ValidateAudience = true, ValidAudience = dxpJwtAudience, ValidateLifetime = true, ValidateIssuerSigningKey = true, IssuerSigningKey = new SymmetricSecurityKey(System.Text.Encoding.UTF8.GetBytes(dxpJwtSigningKey)), ClockSkew = TimeSpan.FromSeconds(30) }; // SignalR's WebSocket transport can't set a custom Authorization header on the handshake — // the JS client instead appends ?access_token=... (SignalR's own convention). Only honor it // on the DXPHub path so REST endpoints keep requiring a real Authorization header. options.Events = new Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerEvents { OnMessageReceived = context => { var accessToken = context.Request.Query["access_token"]; if (!string.IsNullOrEmpty(accessToken) && context.HttpContext.Request.Path.StartsWithSegments("/hubs/dxp")) { context.Token = accessToken; } return Task.CompletedTask; } }; }); builder.Services.AddAuthorization(); // DXPHub — real-time PO/ASN/Invoice status push (Phase 1.6). Options mirror CLAUDE.md's // SignalR registration template / IceImportModule.cs's identical AddSignalR call. builder.Services.AddSignalR(options => { options.EnableDetailedErrors = builder.Environment.IsDevelopment(); options.MaximumReceiveMessageSize = 32 * 1024; options.ClientTimeoutInterval = TimeSpan.FromSeconds(60); options.KeepAliveInterval = TimeSpan.FromSeconds(15); }); builder.Services.Configure(options => { options.AllowSynchronousIO = true; }); builder.Services.AddResponseCompression(o => { o.EnableForHttps = true; o.Providers.Add(); }); builder.Services.AddGB5Telemetry(builder.Configuration, "GB5-DXP"); builder.Services.Configure(builder.Configuration.GetSection("Gb5SystemDTO")); // Universal Attachment System — KYC document upload (Phase 1.2). Registration mirrors // GB5Solution/DMS/DMSSL/DMSModule.cs's identical block (the only existing precedent). builder.Services.AddGB5Storage(builder.Configuration.GetSection("StorageConfiguration")); builder.Services.Configure(builder.Configuration.GetSection(GB5Shared.Attachment.AttachmentPathSettings.Section)); // IAttachmentUploadService/ITemplateResolutionEngine/IAttachmentPathResolutionService now live in // GB5Shared.Attachment (moved out of FrameworkBLL) — not covered by the FrameworkBLL/FrameworkDAL // scan below, since GB5Shared isn't in its FromAssemblies list. builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); // Assembly scan — auto-registers all BLL and DAL implementations. var dxpBllAssembly = Assembly.Load("DXPBLL"); var dxpDalAssembly = Assembly.Load("DXPDAL"); var frameworkBllAssembly = Assembly.Load("FrameworkBLL"); var frameworkDalAssembly = Assembly.Load("FrameworkDAL"); // EXCLUDE IHostedService implementations — discovered via live verification (2026-07-15) that // broadly scanning FrameworkBLL/FrameworkDAL (needed only for UAS's IAttachmentUploadService/ // IAttachmentResolveDAL) also picks up FrameworkBLL's platform-wide background jobs // (OrphanDraftCleanupJob, WorkflowAutoApproveService, SchedulerBackgroundService, GopWorkerService, // etc.) via AsImplementedInterfaces(). The host started them automatically and they immediately // began polling OTHER real tenants' databases (via the shared server-config registry) with queries // never designed to run outside a full PlatformHost context — completely unrelated to DXP. Those // jobs are PlatformHost's responsibility to run once, centrally; a narrow module scanning // FrameworkBLL for one interface must not accidentally activate them. builder.Services.Scan(scan => scan .FromAssemblies(dxpBllAssembly, dxpDalAssembly, frameworkBllAssembly, frameworkDalAssembly) .AddClasses(c => c.Where(t => !t.IsAbstract && !t.IsInterface && !typeof(Microsoft.Extensions.Hosting.IHostedService).IsAssignableFrom(t))) .AsImplementedInterfaces() .WithScopedLifetime()); // List-query infrastructure (Phase 1.3) — auto-registers IListHandler<,> for every // IQueryBuilder<,> found in DXPDAL (currently just VendorPoQB's PO list). builder.Services.AddListInfrastructure(dxpDalAssembly); // DXPHub push notifier (Phase 1.6) — Singleton since it wraps IHubContext, which is itself // singleton/thread-safe; mirrors IceImportModule.cs's identical IIceImportRunNotifier registration. builder.Services.AddSingleton(); // Cross-service HTTP client into the separately-deployed MM microservice (Phase 1.3) — named // client + "Integration:*BaseUrl" config, mirroring PAY/PAYSL/Program.cs's identical pattern // (the only existing precedent for calling another deployed GB5 service). builder.Services.AddHttpClient("MMModule", c => { c.BaseAddress = new Uri(builder.Configuration["Integration:MMModuleBaseUrl"] ?? throw new InvalidOperationException("Integration:MMModuleBaseUrl not configured in appsettings.json")); c.Timeout = TimeSpan.FromSeconds(15); }); var endpointAssemblies = new[] { Assembly.Load("DXPSL"), Assembly.Load("DXPBLL"), Assembly.Load("DXPDAL"), Assembly.Load("GB5Shared") }; builder.Services.AddFastEndpoints(o => { o.Assemblies = endpointAssemblies; }); builder.Services.SwaggerDocument(o => { o.DocumentSettings = s => { s.Title = "GB5 DXP API — Digital Experience Platform"; s.Version = "v1"; }; o.EnableJWTBearerAuth = true; o.ShortSchemaNames = true; }); var app = builder.Build(); // Log the resolved deployment tier loudly — a misconfigured box silently falling through to // Live's un-suffixed connection names is the one real risk this abstraction doesn't otherwise // guard against (see GB5Shared/Deployment). app.Logger.LogInformation("GB5 deployment tier: {Tier}", app.Services.GetRequiredService().TierCode); app.UseForwardedHeaders(); app.UseResponseCompression(); app.UseStaticFiles(); app.UseMiddleware(); // Deliberately no SessionHeartbeatMiddleware here — DXP does not use the internal // self-asserted-LoginDTO + session-liveness trust model (see Part 2 of the DXP plan). It gets its // own signed-JWT + MDXPREFRESHTOKEN session layer instead (UseAuthentication below). app.UseAuthentication(); app.UseAuthorization(); app.UseFastEndpoints(c => { c.Serializer.Options.PropertyNamingPolicy = null; c.Serializer.Options.DictionaryKeyPolicy = null; c.Serializer.Options.AddGB5Converters(); }); app.UseOpenApi(); app.UseSwaggerUi(o => { o.Path = "/DXPDocumentation"; o.DocumentPath = "/swagger/{documentName}/swagger.json"; o.TransformToExternalPath = (internalUiRoute, _) => "/dxp" + internalUiRoute; }); app.MapHub("/hubs/dxp"); app.UseCloudEvents(); app.MapSubscribeHandler(); app.MapControllers(); app.MapGet("/", () => "Hello from GB5 DXP .NET 9 API!"); app.Run();