using System.Reflection; using System.Text; 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.EventLogPublish; using GB5Shared.GB5CommonFunction; using GB5Shared.GenerateAutoNumber; using GB5Shared.GOP.Qualifier; using GB5Shared.QueryExecutor; using GB5Shared.ScanEvent; using GB5Shared.Validation; using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.Extensions.Caching.Hybrid; using OpenTelemetry; using OpenTelemetry.Metrics; using OpenTelemetry.Resources; using OpenTelemetry.Trace; using Quartz; // ------------------------- // Bootstrap // ------------------------- 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}"); // ------------------------- // Console Encoding // ------------------------- Console.OutputEncoding = Encoding.UTF8; // ------------------------- // 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 // ------------------------- // ------------------------- // Application Services // ------------------------- builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); // Missing from this SL until now — every other module's SL Program.cs registers these two // (see e.g. MMSL/CRMSL/PayRollSL Program.cs). Without them, any endpoint whose BLL chain // reaches VoucherNumberService (CRM) or EventLogPublish (FM's own AssetStateBLL/ScanZoneBLL // etc.) fails to construct — FastEndpoints eagerly builds every endpoint at startup, so this // blocked the whole service from starting standalone, not just those specific endpoints. builder.Services.AddScoped(); builder.Services.AddScoped(); // AssetActivitySchedulerJob (below) resolves CRMBLL's ITaskBLL, which itself needs // DocumentNumberService (uses VoucherNumberService, registered above) and the open-generic // BaseEntityAppService<> - neither is covered by the narrowed Admin/CRM scan below. // BaseEntityAppService<> in turn needs IQualifierFacade/IWorkFlowEngine/IOutBox - full chain // replicated from PayRollSL/PayRollModule.cs's identical registration set. builder.Services.AddScoped(); builder.Services.AddScoped(typeof(GB5Shared.EntityHandler.BaseEntityAppService<>), typeof(GB5Shared.EntityHandler.BaseEntityAppService<>)); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddGB5QualifierEngine(); builder.Services.AddScoped(); builder.Services.Configure( builder.Configuration.GetSection("Gb5SystemDTO")); // ------------------------- // Kestrel // ------------------------- builder.Services.Configure(options => { options.AllowSynchronousIO = true; }); // -- OpenTelemetry (Tracing + Metrics) ---------------------------------------- builder.Services.AddGB5Telemetry(builder.Configuration, "GB5-FM"); // ------------------------- // Assembly Scan for DI // ------------------------- var fmBLLAssembly = Assembly.Load("FMBLL"); var fmDALAssembly = Assembly.Load("FMDAL"); builder.Services.Scan(scan => scan .FromAssemblies(fmBLLAssembly, fmDALAssembly) .AddClasses(c => c.Where(t => !t.IsAbstract && !t.IsInterface && (t.Namespace == null || !t.Namespace.Contains(".DTO")) && !typeof(Quartz.IJob).IsAssignableFrom(t) // IScanEventHandler implementations are registered explicitly via // AddScanEventHandlers below — excluded here to avoid a double // registration that would make ScanEventDispatcher see each handler twice. && !typeof(GB5Shared.ScanEvent.IScanEventHandler).IsAssignableFrom(t))) .AsImplementedInterfaces() .WithScopedLifetime()); // Cross-module reuse: AssetActivitySchedulerJob resolves AdminBLL's IBIZTransactionTypeBLL // and CRMBLL's ITaskBLL (creates TTASK rows). AdminBLL/AdminDAL and CRMBLL/CRMDAL are // deliberately NOT blanket-scanned - they contain many other Admin/CRM-specific services // (OrganizationUnitBLL needs IHttpClientFactory, BulkIceImportBLL needs // IFileToDataTableService, etc.) that need config/DI this standalone SL never sets up. // Only the two classes this job actually needs are registered here, narrowed by class name // (same pattern as SwModule.cs's FrameworkBLL.ServerConfigBLL scan). var adminBLLAssembly = Assembly.Load("AdminBLL"); var adminDALAssembly = Assembly.Load("AdminDAL"); var crmBLLAssembly = Assembly.Load("CRMBLL"); var crmDALAssembly = Assembly.Load("CRMDAL"); builder.Services.Scan(scan => scan .FromAssemblies(adminBLLAssembly, adminDALAssembly, crmBLLAssembly, crmDALAssembly) .AddClasses(c => c.Where(t => !t.IsAbstract && !t.IsInterface && (t.Name == "BIZTransactionTypeBLL" || t.Name == "BIZTransactionTypeDAL" || t.Name == "AdvanceRightsDAL" || t.Name == "TaskBLL" || t.Name == "TaskDAL"))) .AsImplementedInterfaces() .WithScopedLifetime()); // ------------------------- // RAM — Scan Event framework (IScanEventDispatcher + this module's IScanEventHandler(s)) // ------------------------- builder.Services.AddScanEventHandlers(fmBLLAssembly); // ------------------------- // Quartz.NET — AssetActivitySchedulerJob // ------------------------- builder.Services.AddQuartz(q => { q.UseSimpleTypeLoader(); q.UseInMemoryStore(); var assetActivitySchedulerKey = new JobKey("AssetActivitySchedulerJob"); q.AddJob(opts => opts.WithIdentity(assetActivitySchedulerKey)); q.AddTrigger(opts => opts.ForJob(assetActivitySchedulerKey) .WithIdentity("AssetActivitySchedulerJob-trigger") .WithSimpleSchedule(s => s.WithIntervalInMinutes(5).RepeatForever())); }); builder.Services.AddQuartzHostedService(opt => opt.WaitForJobsToComplete = true); // ------------------------- // FastEndpoints // ------------------------- builder.Services.AddFastEndpoints(); // ------------------------- // Swagger // ------------------------- builder.Services.SwaggerDocument(o => { o.DocumentSettings = s => { s.Title = "GB5 FM Service"; s.Version = "v1"; s.Description = "GB5 Financial Management Microservice"; }; }); // ------------------------- // Build App // ------------------------- var app = builder.Build(); // ------------------------- // Middleware // ------------------------- app.UseCloudEvents(); // FIX: Was app.MapFastEndpoints() — wrong method, no serializer config app.UseMiddleware(); app.UseMiddleware(); app.UseFastEndpoints(c => { c.Serializer.Options.PropertyNamingPolicy = null; c.Serializer.Options.DictionaryKeyPolicy = null; c.Serializer.Options.AddGB5Converters(); }); app.MapControllers(); app.MapSubscribeHandler(); // ------------------------- // Swagger UI (All Environments) // ------------------------- app.UseOpenApi(); app.UseSwaggerUi(o => { o.Path = "/GB5Documentation"; o.DocumentPath = "/swagger/{documentName}/swagger.json"; o.TransformToExternalPath = (internalUiRoute, _) => "/fms" + internalUiRoute; }); // ------------------------- // Test Endpoint // ------------------------- app.MapGet("/", () => "Hello from GB5 FM Service (.NET 9)"); // ------------------------- // HTTPS & Authorization // ------------------------- app.UseAuthorization(); // ------------------------- // Run the Application // ------------------------- app.Run();