using System.Text; using FastEndpoints; using FastEndpoints.Swagger; using GB5Shared.Auth.Jwt; using GB5Shared.Connection; using GB5Shared.DateConverter; using static GB5Shared.DateConverter.GB5JsonOptions; using GB5Shared.DTO.Framework.CommonConfig; using GB5Shared.GB5CommonFunction; using GB5Shared.GenerateAutoNumber; using GB5Shared.Hosting; using GB5Shared.Middleware; using GB5Shared.QueryExecutor; using GB5Shared.Storage; using GB5Shared.Swagger; using GB5Shared.Telemetry; using GB5Shared.Validation; using Microsoft.AspNetCore.HttpOverrides; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.ResponseCompression; using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.Extensions.Caching.Hybrid; using Microsoft.Extensions.Logging; using Quartz; Console.OutputEncoding = Encoding.UTF8; // Register Dapper type handlers for DateOnly and DateTimeOffset once at startup. // Must run before any Dapper query executes - Dapper type handlers are process-global. GB5DapperTypeHandlers.Register(); var builder = WebApplication.CreateBuilder(args); var appPort = builder.Configuration.GetValue("AppPort"); builder.WebHost.UseUrls($"http://0.0.0.0:{appPort}"); // Dapr & Controllers builder.Services.AddDaprClient(); builder.Services.AddControllers() .AddDapr() .AddJsonOptions(options => { options.JsonSerializerOptions.PropertyNamingPolicy = null; options.JsonSerializerOptions.DictionaryKeyPolicy = null; options.JsonSerializerOptions.AddGB5Converters(); }); builder.Services.Configure(options => { options.SuppressModelStateInvalidFilter = true; }); builder.Services.ConfigureHttpJsonOptions(options => { options.SerializerOptions.PropertyNamingPolicy = null; options.SerializerOptions.DictionaryKeyPolicy = null; options.SerializerOptions.AddGB5Converters(); }); // Caching #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(); // CORS // HttpContext (required by SkillManagement) builder.Services.AddHttpContextAccessor(); // Generic HttpClient builder.Services.AddHttpClient(); // Common application services shared across all HRFinance modules builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); // GB5Shared.Attachment/Storage/FileUpload/Signatory/DigitalSignature — required by // TMSBLL.Certificate.CertificatePdfPipeline (certificate PDF signing/attachment upload). None of // this is picked up by ModuleLoader's assembly scan (GB5Shared isn't a module DLL), so it must be // registered explicitly here — same pattern as standalone TMSSL/Program.cs's own registration of // these exact types, which isn't used once TMS is hosted inside this combined host instead. // Confirmed missing live via HRFinanceHost's /health DI pre-flight check. builder.Services.Configure( builder.Configuration.GetSection(GB5Shared.Attachment.AttachmentPathSettings.Section)); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddGB5Storage(builder.Configuration.GetSection("StorageConfiguration")); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); // Shared configuration binding builder.Services.Configure(builder.Configuration.GetSection("Gb5SystemDTO")); // Kestrel builder.Services.Configure(options => { options.AllowSynchronousIO = true; }); // Response Compression (required by SkillManagement) builder.Services.AddResponseCompression(o => { o.EnableForHttps = true; o.Providers.Add(); }); // OpenTelemetry - single service name for the consolidated host builder.Services.AddGB5Telemetry(builder.Configuration, "GB5-HRFINANCE"); // Module discovery + registration - fully dynamic, no hardcoded module list. Every module DLL // referenced by HRFinanceHost.csproj is discovered from the published output folder, registered, // and safety-checked here. A module that throws during Register() (e.g. missing config) or whose // endpoints have an unresolvable DI dependency is logged and excluded - it never takes the rest // of HRFinanceHost down. To add a module to this host: add a to // HRFinanceHost.csproj. Nothing below needs to change. // (FLSSL is also referenced by PlatformHost - TMSBLL.TmsFeedbackBLL depends on FLSBLL.IFlsInstanceBLL.) using var startupLoggerFactory = LoggerFactory.Create(b => b.AddConsole()); var startupLogger = startupLoggerFactory.CreateLogger("HRFinanceHost.ModuleLoader"); var moduleLoad = ModuleLoader.LoadAll(builder.Services, builder.Configuration, startupLogger, "GB5Shared"); // Quartz.NET — TMSBLL.KpiPosting.TmsKpiPostingJob (nightly TMS KPI posting into TKPIVALUE). // Mirrors FMSL/Program.cs's own AddQuartz registration for AssetActivitySchedulerJob — that is // this codebase's only other real, running per-tenant scheduled job (JobEngine/MJOBDEFINE is // confirmed unprovisioned in every host). builder.Services.AddQuartz(q => { q.UseSimpleTypeLoader(); q.UseInMemoryStore(); var tmsKpiPostingJobKey = new JobKey("TmsKpiPostingJob"); q.AddJob(opts => opts.WithIdentity(tmsKpiPostingJobKey)); q.AddTrigger(opts => opts.ForJob(tmsKpiPostingJobKey) .WithIdentity("TmsKpiPostingJob-trigger") .WithCronSchedule("0 0 1 * * ?")); // nightly, 01:00 UTC }); builder.Services.AddQuartzHostedService(opt => opt.WaitForJobsToComplete = true); // AddFastEndpoints() itself eagerly scans moduleLoad.FastEndpointsAssemblies for IEndpoint types // (not deferred to UseFastEndpoints) — FastEndpoints' internal Assembly.GetTypes() call isn't // ours to wrap, so a ReflectionTypeLoadException here previously surfaced with no detail beyond // "Unable to load one or more of the requested types." Log the real LoaderExceptions before // rethrowing so the actual missing/mismatched dependency is visible instead of having to guess. try { builder.Services.AddFastEndpoints(o => { o.Assemblies = moduleLoad.FastEndpointsAssemblies; o.Filter = moduleLoad.EndpointFilter; }); } catch (System.Reflection.ReflectionTypeLoadException rtle) { foreach (var loaderEx in rtle.LoaderExceptions) startupLogger.LogError(loaderEx, "AddFastEndpoints() type-load failure detail"); throw; } // A few PayRollSL endpoints set a custom hardcoded OperationId (bypassing the usual // assembly-name-prefixed auto-generated one) and have routes with no module-name segment. var pathAliases = new Dictionary(StringComparer.OrdinalIgnoreCase) { ["PayProcess"] = "PayRoll", ["TransferEmployeeAdvance"] = "PayRoll", ["Get-SubPayProduction"] = "PayRoll", }; // Swagger - one native document per module bundled into this Host, instead of a single combined // document covering all of them - see GB5Shared.Swagger.ModuleSwaggerRegistration. builder.Services.AddPerModuleSwaggerDocuments(moduleLoad, "GB5 HRFinance API", pathAliases); #region ── Multi-tenant Keycloak JWT Bearer (GB5 Repo-Wide Authentication Hardening) ── // Additive only — this host had NO AddAuthentication()/UseAuthentication() at all before this. // Zero behavior change for every existing caller: AllowAnonymous() endpoints (still the default // everywhere) ignore auth schemes entirely, and BaseEndPoint.GetLoginDTOFromRequestAsync's // dual-mode bridge falls straight through to today's trusted-header path whenever no valid // Keycloak token is presented — which is every request until a tenant's MSERVERCONFIG.AUTHMODE is // deliberately set to Keycloak. Same "oidc" named client / MultiTenantOidcJwksCache / // GB5Shared.Auth.Jwt.KeycloakLoginDTOResolver pattern already proven live in FrameworkSL — see // that Program.cs for the fuller design comment. builder.Services.AddHttpClient("oidc") .ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler { ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator }); builder.Services.AddGB5MultiTenantJwtBearerSupport(); builder.Services.AddScoped(); builder.Services.AddAuthentication(Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { options.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters { ValidateIssuer = false, ValidateAudience = false, ValidateLifetime = true, ValidateIssuerSigningKey = true, }; options.Events = GB5Shared.Auth.Jwt.MultiTenantJwtBearerEvents.Build(); }); #endregion var app = builder.Build(); app.UseGatewayPrefixForwarding(); // Middleware pipeline - union of all modules in correct order app.UseForwardedHeaders(); // SkillManagement requires this first app.UseResponseCompression(); // SkillManagement app.UseAuthentication(); app.UseAuthorization(); app.UseCloudEvents(); app.UseMiddleware(); app.UseMiddleware(); // UseFastEndpoints() is where the library actually scans moduleLoad.FastEndpointsAssemblies for // IEndpoint types and wires them up — ModuleLoader's own pre-flight checks reduce the chance of a // bad assembly reaching this point, but FastEndpoints' internal Assembly.GetTypes() call isn't // ours to wrap, so a ReflectionTypeLoadException here previously surfaced with no detail beyond // "Unable to load one or more of the requested types." Log the real LoaderExceptions before // rethrowing so the actual missing/mismatched dependency is visible instead of having to guess. try { app.UseFastEndpoints(c => { c.Serializer.Options.PropertyNamingPolicy = null; c.Serializer.Options.DictionaryKeyPolicy = null; c.Serializer.Options.AddGB5Converters(); }); } catch (System.Reflection.ReflectionTypeLoadException rtle) { foreach (var loaderEx in rtle.LoaderExceptions) app.Logger.LogError(loaderEx, "UseFastEndpoints() type-load failure detail"); throw; } moduleLoad.MapEndpoints(app, app.Logger); app.MapControllers(); app.MapSubscribeHandler(); // Swagger UI - one page per module; /GB5Documentation redirects to the requesting gateway alias's // own module page (see GB5Shared.Swagger.ModuleSwaggerRegistration). app.MapPerModuleSwaggerUi(moduleLoad); moduleLoad.MapModuleHealth(app, "HRFinance"); app.MapGet("/", () => $"Hello from GB5 HRFinance Host (.NET 9) - {moduleLoad.BannerText()}"); app.Run();