using FastEndpoints; using FastEndpoints.Swagger; using GB5Shared.Authorization; using GB5Shared.Connection; using GB5Shared.DaprCache; using GB5Shared.DateConverter; using GB5Shared.Deployment; using GB5Shared.DTO.Framework.CommonConfig; using GB5Shared.DTO.Framework.Login; using GB5Shared.EntityHandler; using GB5Shared.GB5CommonFunction; using GB5Shared.GB5Library.Qualifier; using GB5Shared.GenerateAutoNumber; using GB5Shared.GOP.Qualifier; using GB5Shared.ListQuery; using GB5Shared.PubSub.OutBox; using GB5Shared.QueryExecutor; using GB5Shared.Telemetry; using GB5Shared.Validation; using GB5Shared.WorkFlow.WorkFlowEngine; using GB5Shared.WorkFlow.WorkFlowRunTime; using Microsoft.AspNetCore.ResponseCompression; using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.Extensions.Caching.Hybrid; using Microsoft.Extensions.Hosting; using Newtonsoft.Json; using Quartz; using OpenTelemetry; using OpenTelemetry.Metrics; using OpenTelemetry.Resources; using OpenTelemetry.Trace; using SwSL.EndPoints.ConnectionValidator; using System.Reflection; using System.Text; using static GB5Shared.DateConverter.GB5JsonOptions; var builder = WebApplication.CreateBuilder(args); Console.OutputEncoding = Encoding.UTF8; // ── 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}"); // ------------------------- // DAPR + CONTROLLERS // ------------------------- builder.Services.AddDaprClient(); builder.Services.AddControllers() .AddDapr() .AddJsonOptions(options => { options.JsonSerializerOptions.PropertyNamingPolicy = null; options.JsonSerializerOptions.DictionaryKeyPolicy = null; options.JsonSerializerOptions.AddGB5Converters(); }); // ------------------------- // JSON GLOBAL // ------------------------- builder.Services.ConfigureHttpJsonOptions(options => { options.SerializerOptions.PropertyNamingPolicy = null; options.SerializerOptions.DictionaryKeyPolicy = null; options.SerializerOptions.AddGB5Converters(); }); // ------------------------- // CACHE // ------------------------- #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(); // ------------------------- // HTTP CONTEXT // ------------------------- builder.Services.AddHttpContextAccessor(); // ------------------------- // LOGIN HEADER INJECTION // ------------------------- builder.Services.AddScoped(sp => { var httpContext = sp.GetRequiredService().HttpContext; if (httpContext != null && httpContext.Request.Headers.TryGetValue("Login", out var loginHeader)) { return JsonConvert.DeserializeObject(loginHeader!)!; } return new LoginDTO(); }); // ------------------------- // SERVICES // ------------------------- builder.Services.AddScoped(sp => { var httpContext = sp.GetRequiredService().HttpContext; if (httpContext != null && httpContext.Request.Headers.TryGetValue("Login", out var loginHeader)) { return JsonConvert.DeserializeObject(loginHeader!)!; } return new LoginDTO(); }); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(typeof(BaseEntityAppService<>)); builder.Services.AddScoped(); builder.Services.AddGB5QualifierEngine(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddMenuRightsAuthorization(); builder.Services.AddScoped(); builder.Services.Configure( builder.Configuration.GetSection("Gb5SystemDTO")); // Deployment tier (Singleton — GB5:Environment=Dev|QC|Live). SqlWorkbench itself doesn't // resolve a tier-suffixed connection name (Dev/QC share one Gb5System, tiered via // SW.MSWCLIENTDATABASE.TIER instead — see Docs/SqlWorkbench-DB-Settings-Guide.md); this // registration exists so IGB5Environment is available here too for consistency/logging. builder.Services.AddGB5Environment(builder.Configuration); // Named HttpClient for Entitlement-aware provisioning/rollout gating (tracker §40) — reverse // direction of EntitlementSL/Program.cs's own "SqlWorkbench"-named client. No project reference // to EntitlementDAL/EntitlementBLL exists or should exist (§31/§38/§40's own "no cross-module // BLL reference" rule). Base URL is unset by default (RolloutCampaignBLL fails loudly, not // silently, if a call is attempted with no base address configured). builder.Services.AddHttpClient("Entitlement", c => { var baseUrl = builder.Configuration["Integration:EntitlementBaseUrl"]; if (!string.IsNullOrWhiteSpace(baseUrl)) c.BaseAddress = new Uri(baseUrl); }); // Client-database provisioning config (ClientDatabaseProvisioner) — backup/data/log file paths // for RESTORE/CREATE DATABASE, plus the Vault client used to store the three per-database // contained-user credentials it creates. Registration mirrors DXPSL/Program.cs's identical // Vault block (the proven precedent for this pattern in this repo). builder.Services.Configure( builder.Configuration.GetSection(SwBLL.Provisioning.SqlServerProvisioningOptions.SectionName)); builder.Services.AddSingleton(sp => { var authMethod = new VaultSharp.V1.AuthMethods.Token.TokenAuthMethodInfo(builder.Configuration["Vault:Token"]!); var settings = new VaultSharp.VaultClientSettings(builder.Configuration["Vault:Address"]!, authMethod); return new VaultSharp.VaultClient(settings); }); // ------------------------- // KESTREL // ------------------------- builder.Services.Configure(options => { options.AllowSynchronousIO = true; }); // ------------------------- // RESPONSE COMPRESSION // ------------------------- builder.Services.AddResponseCompression(o => { o.EnableForHttps = true; o.Providers.Add(); }); // ── OpenTelemetry (Tracing + Metrics) ──────────────────────────────── builder.Services.AddGB5Telemetry(builder.Configuration, "GB5-SQLWORKBENCH"); // ------------------------- // DI SCAN // ------------------------- var swBLLAssembly = Assembly.Load("SwBLL"); var swDALAssembly = Assembly.Load("SwDAL"); // ServerConfigBLL/ServerConfigDAL (ClientDatabaseProvisioner's MSERVER/MSERVERCONFIG registration // dependency) now live in SwBLL/SwDAL themselves, so the scan below already registers them — no // separate Framework scan needed. builder.Services.Scan(scan => scan .FromAssemblies(swBLLAssembly, swDALAssembly) .AddClasses(c => c.Where(t => !t.IsAbstract && !t.IsInterface && !t.Name.EndsWith("ListQuery") && !t.Name.EndsWith("Criteria") && !t.Name.EndsWith("ListDTO") && !t.Name.EndsWith("DTO"))) .AsImplementedInterfaces() .WithScopedLifetime()); // Auto-registers GenericListHandler for each IQueryBuilder in SwDAL builder.Services.AddListInfrastructure(swDALAssembly); // RolloutCampaignWorkerJob (every 2 min) and ChangeRequestExecutionWorkerJob (every 1 min) used // to run here as standalone in-memory Quartz jobs. Cut over to Track A (GB5Framework's real // MJOBDEFINE/TSCHEDULER scheduler, tracker §45) as of this pass — their logic already lived in // RolloutCampaignBLL.ProcessDueTargetsAsync/ChangeRequestBLL.ProcessQueuedExecutionsAsync (the // Quartz jobs were already thin wrappers over these), now dispatched via // GET /RolloutCampaign/RunRolloutCampaignWorker and GET /ChangeRequest/RunChangeRequestExecutionWorker // instead. SqlWorkbench has no other Quartz usage — the whole AddQuartz/AddQuartzHostedService // registration is removed, not just these two jobs. // ------------------------- // FAST ENDPOINTS // ------------------------- var endpointAssemblies = new[] { Assembly.Load("SwSL"), Assembly.Load("SwBLL"), Assembly.Load("SwDAL"), Assembly.Load("GB5Shared") }; builder.Services.AddFastEndpoints(o => { o.Assemblies = endpointAssemblies; }); // ------------------------- // SWAGGER // ------------------------- builder.Services.SwaggerDocument(o => { o.DocumentSettings = s => { s.Title = "GB5 SQL Workbench API"; s.Version = "v1"; s.Description = "GB5 SQL Workbench Microservice"; }; o.EnableJWTBearerAuth = false; o.ShortSchemaNames = true; }); // ------------------------- // BUILD // ------------------------- 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); // ------------------------- // MIDDLEWARE // ------------------------- app.UseForwardedHeaders(); app.UseResponseCompression(); app.UseCloudEvents(); app.UseMiddleware(); app.UseMiddleware(); app.UseFastEndpoints(c => { c.Serializer.Options.PropertyNamingPolicy = null; c.Serializer.Options.DictionaryKeyPolicy = null; c.Serializer.Options.AddGB5Converters(); c.Serializer.Options.AddGB5Converters(); }); app.MapControllers(); app.MapSubscribeHandler(); // ------------------------- // SWAGGER UI // ------------------------- app.UseOpenApi(); app.UseSwaggerUi(o => { o.Path = "/GB5Documentation"; o.DocumentPath = "/swagger/{documentName}/swagger.json"; o.TransformToExternalPath = (internalUiRoute, _) => "/sws" + internalUiRoute; }); // ------------------------- app.MapGet("/", () => "Hello from GB5 SQL Workbench API!"); app.UseAuthorization(); app.Run();