using System.Reflection; using System.Text; using GB5Shared.GOP.Qualifier; using Dapr; using FastEndpoints; using FastEndpoints.Swagger; using GB5Shared.Connection; using GB5Shared.Telemetry; using GB5Shared.DateConverter; using static GB5Shared.DateConverter.GB5JsonOptions; using GB5Shared.DTO.Framework.CommonConfig; using GB5Shared.DTO.Framework.Login; using GB5Shared.ExcelExport; using GB5Shared.GB5CommonFunction; using GB5Shared.GenerateAutoNumber; using GB5Shared.ListQuery; using GB5Shared.QueryExecutor; using GB5Shared.DaprCache; using GB5Shared.EntityHandler; using GB5Shared.EventLogPublish; using GB5Shared.GB5Library.Qualifier; using GB5Shared.PubSub.OutBox; using GB5Shared.ScanIdentifier; using GB5Shared.WorkFlow.WorkFlowEngine; using GB5Shared.WorkFlow.WorkFlowRunTime; using GB5Shared.Validation; using Microsoft.AspNetCore.HttpOverrides; using Microsoft.AspNetCore.ResponseCompression; using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.Extensions.Caching.Hybrid; using MMDAL.DTO.MRPRun; using MMDAL.DTO.Scheduling; using MMSL.Hubs; using MMSL.Services; using Newtonsoft.Json; // ------------------------- // Bootstrap // ------------------------- var builder = WebApplication.CreateBuilder(args); // ------------------------- // Console Encoding // ------------------------- 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 Setup // ------------------------- builder.Services.AddDaprClient(); builder.Services.AddControllers() .AddDapr() .AddJsonOptions(options => { options.JsonSerializerOptions.PropertyNamingPolicy = null; options.JsonSerializerOptions.DictionaryKeyPolicy = null; options.JsonSerializerOptions.AddGB5Converters(); }); // ------------------------- // JSON Global (Minimal API / HttpContext) // ------------------------- 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 Policy // ------------------------- // ------------------------- // HTTP Context // ------------------------- builder.Services.AddHttpContextAccessor(); // ------------------------- // Login Header (per-request from HTTP header) // ------------------------- // ⚠ FOR HTTP ENDPOINTS ONLY. // Returns empty DTO instead of throwing — prevents crashes in // background scopes that transitively touch this registration. 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(); }); // ------------------------- // Application Services // ------------------------- 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(); builder.Services.AddScoped(typeof(BaseEntityAppService<>), typeof(BaseEntityAppService<>)); builder.Services.AddScoped(); builder.Services.AddScoped(); // MMHeadBLL's row-level incremental save (SessionGuid path) consumes the Draft framework's // promotion helpers (GB5Shared.Draft — relocated here from FrameworkBLL/FrameworkDAL). // Registered explicitly rather than via assembly scan, matching this service's other // individually-registered GB5Shared utilities above. builder.Services.AddScoped(); builder.Services.AddScoped(); // File Import Services (GB5Shared) builder.Services.AddScoped(); // Report Export Services (GB5Shared) builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddHttpClient(); // ------------------------- // OutBox // ------------------------- builder.Services.AddScoped(); // ------------------------- // Workflow Engine // ------------------------- builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); // ------------------------- // Qualifier Engine // ------------------------- builder.Services.AddScoped(); builder.Services.AddGB5QualifierEngine(); // ------------------------- // SignalR // ------------------------- builder.Services.AddSignalR(options => { options.EnableDetailedErrors = builder.Environment.IsDevelopment(); options.MaximumReceiveMessageSize = 32 * 1024; options.ClientTimeoutInterval = TimeSpan.FromSeconds(60); options.KeepAliveInterval = TimeSpan.FromSeconds(15); }); // MRP Run processing service — singleton; uses IServiceScopeFactory for scoped DI builder.Services.AddSingleton(); // Scheduling Run processing service — singleton; same pattern as MRPRunProcessingService builder.Services.AddSingleton(); // ------------------------- // Queue Processor + Handlers (MRPRUN, SCHEDULINGRUN) // ------------------------- builder.Services.AddHostedService(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.Configure( builder.Configuration.GetSection("Gb5SystemDTO")); // ------------------------- // 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-MM"); // ------------------------- // Assembly Scan for DI // ------------------------- var mmBLLAssembly = Assembly.Load("MMBLL"); var mmDALAssembly = Assembly.Load("MMDAL"); var adminBLLAssembly = Assembly.Load("AdminBLL"); var adminDALAssembly = Assembly.Load("AdminDAL"); builder.Services.Scan(scan => scan .FromAssemblies(mmBLLAssembly, mmDALAssembly, adminBLLAssembly, adminDALAssembly) .AddClasses(c => c.Where(t => !t.IsAbstract && !t.IsInterface && t.Namespace != null && !t.Namespace.Contains(".DTO") && !typeof(Exception).IsAssignableFrom(t) && t.GetMethod("$") == null)) // exclude record types .AsImplementedInterfaces() .WithScopedLifetime()); // IListDispatcher + GenericListHandler + IQueryBuilder registrations // Scans MMDAL for all IQueryBuilder implementations and auto-registers // GenericListHandler as IListHandler for each one found. builder.Services.AddListInfrastructure(mmDALAssembly); // Generic Scan-Identifier & Label Platform (GB5Shared/ScanIdentifier) — the dispatcher only; // MM's own IScanTargetResolver implementations (IndentDetailResolver) are picked up by the // broader MMBLL/MMDAL assembly Scan() above already, so passing an assembly here would // double-register them. builder.Services.AddScanTargetResolvers(); builder.Services.AddScoped(); // ------------------------- // FastEndpoints // ------------------------- var endpointAssemblies = new[] { Assembly.Load("MMSL"), Assembly.Load("MMBLL"), Assembly.Load("MMDAL"), Assembly.Load("GB5Shared") }; builder.Services.AddFastEndpoints(o => { o.Assemblies = endpointAssemblies; }); // ------------------------- // Swagger // ------------------------- builder.Services.SwaggerDocument(o => { o.DocumentSettings = s => { s.Title = "GB5 MM API"; s.Version = "v1"; }; o.EnableJWTBearerAuth = false; o.ShortSchemaNames = true; }); // ------------------------- // Build App // ------------------------- var app = builder.Build(); // ------------------------- // Middleware // ------------------------- // 1. Reverse-proxy header forwarding (must be first) app.UseForwardedHeaders(); // 2. Routing — explicit call required before CORS so OPTIONS preflight is matched // to the route before any custom middleware executes. app.UseRouting(); // 3. CORS (after routing, before tracing) // 4. Response Compression app.UseResponseCompression(); // 5. Tracing enrichment — runs after routing+CORS so the root OTel span is active; // adds correlation ID, client IP, Dapr source, WIP marker to the active span. app.UseMiddleware(); // 6. Session heartbeat app.UseMiddleware(); // 7. FastEndpoints app.UseFastEndpoints(c => { c.Serializer.Options.PropertyNamingPolicy = null; c.Serializer.Options.DictionaryKeyPolicy = null; c.Serializer.Options.AddGB5Converters(); // FastEndpoints' default RequestDeserializer unconditionally JSON-parses the request body. // Endpoints like GetMMHead bind purely via [QueryParam]/[FromHeader] and have no body, but // Swagger UI / Postman still send Content-Type: application/json with an empty body on GET — // JsonSerializer then throws "input does not contain any JSON tokens", surfacing as a // spurious 400 SerializerErrors response before ExecuteAsync ever runs. Skip JSON parsing // when there is no body to read. c.Serializer.RequestDeserializer = async (req, tRequest, jsonContext, ct) => { if (req.ContentLength is null or 0) return System.Runtime.CompilerServices.RuntimeHelpers.GetUninitializedObject(tRequest); return await System.Text.Json.JsonSerializer.DeserializeAsync(req.Body, tRequest, c.Serializer.Options, ct); }; }); // 8. Dapr CloudEvents unwrapper — must be after FastEndpoints so Dapr pub/sub // controllers can deserialize CloudEvent envelopes; placed after tracing so // every Dapr topic subscription also gets a root span. app.UseCloudEvents(); // 9. MVC Controllers app.MapControllers(); // 10. Dapr subscribe handler app.MapSubscribeHandler(); // ------------------------- // Swagger UI (All Environments) // ------------------------- app.UseOpenApi(); app.UseSwaggerUi(o => { o.Path = "/GB5Documentation"; o.DocumentPath = "/swagger/{documentName}/swagger.json"; o.TransformToExternalPath = (internalUiRoute, _) => "/mms" + internalUiRoute; }); // ------------------------- // Test Endpoint // ------------------------- app.MapGet("/", () => "Hello from GB5 MM in .NET 9 API!"); // ------------------------- // SignalR Hubs // ------------------------- app.MapHub("/hubs/mrprun"); app.MapHub("/hubs/schedulingrun"); app.MapHub("/hubs/operatorconsole"); // ------------------------- // Dapr Pub/Sub Subscription Handlers // ------------------------- //app.MapPost("/mrprun/trigger", // [Topic("pubsub", "mrprun.trigger")] // async (MRPRunTriggerDTO dto, MRPRunProcessingService svc, CancellationToken ct) // => await svc.ProcessAsync(dto, ct)); //app.MapPost("/mrprun/combined/trigger", // [Topic("pubsub", "mrprun.combined.trigger")] // async (MRPRunTriggerDTO dto, MRPRunProcessingService svc, CancellationToken ct) // => await svc.ProcessCombinedAsync(dto, ct)); app.MapPost("/scheduling/trigger", [Topic("pubsub", "schedulingrun.trigger")] async (SchedulingRunTriggerDTO dto, SchedulingRunProcessingService svc, CancellationToken ct) => await svc.ProcessAsync(dto, ct)); // ------------------------- // HTTPS & Authorization // ------------------------- //app.UseHttpsRedirection(); app.UseAuthorization(); // ------------------------- // Run the Application // ------------------------- app.Run();