using System.Reflection; using System.Text; 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.GB5CommonFunction; using GB5Shared.QueryExecutor; using GB5Shared.DaprCache; using GB5Shared.PubSub.OutBox; using GB5Shared.Validation; using GB5Shared.Export; using JobEngineBLL.Interfaces; using JobEngineSL.Hubs; using JobEngineSL.Services; using Microsoft.AspNetCore.HttpOverrides; using Microsoft.AspNetCore.ResponseCompression; using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.Extensions.Caching.Hybrid; using Newtonsoft.Json; 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.OutputEncoding = Encoding.UTF8; // ------------------------- // Dapr & Controllers // ------------------------- builder.Services.AddDaprClient(); 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(); }); // ------------------------- // 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(); builder.Services.AddHttpContextAccessor(); // ------------------------- // LoginDTO (per-request from HTTP header) // ------------------------- builder.Services.AddScoped(sp => { var ctx = sp.GetRequiredService().HttpContext; if (ctx != null && ctx.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.AddHttpClient("jobengine", c => { c.Timeout = TimeSpan.FromMinutes(5); }); builder.Services.AddHttpClient(); // ------------------------- // Report Export // ------------------------- builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); // ------------------------- // Quartz.NET — clustered persistent scheduler // ------------------------- var quartzConnStr = builder.Configuration.GetConnectionString("QuartzStore") ?? throw new InvalidOperationException("ConnectionStrings:QuartzStore is required for Quartz persistent store"); builder.Services.AddQuartz(q => { q.UseSimpleTypeLoader(); q.UseDefaultThreadPool(tp => tp.MaxConcurrency = 20); q.UsePersistentStore(s => { s.UseProperties = true; s.RetryInterval = TimeSpan.FromSeconds(15); s.UseSqlServer(quartzConnStr); s.UseJsonSerializer(); s.UseClustering(); }); }); builder.Services.AddQuartzHostedService(opt => opt.WaitForJobsToComplete = true); // ------------------------- // SignalR // ------------------------- builder.Services.AddSignalR(options => { options.EnableDetailedErrors = builder.Environment.IsDevelopment(); options.MaximumReceiveMessageSize = 32 * 1024; options.ClientTimeoutInterval = TimeSpan.FromSeconds(60); options.KeepAliveInterval = TimeSpan.FromSeconds(15); }); // ------------------------- // QuartzSyncService (singleton — holds IScheduler reference) // Registered as both the concrete type AND the BLL interface so that // the BLL layer (IQuartzSyncService) and the startup loader can both resolve it. // ------------------------- builder.Services.AddSingleton(); builder.Services.AddSingleton(sp => sp.GetRequiredService()); // ------------------------- // Background Services // ------------------------- builder.Services.AddHostedService(); // ------------------------- // Kestrel // ------------------------- builder.Services.Configure(options => { options.AllowSynchronousIO = true; }); builder.Services.AddResponseCompression(o => { o.EnableForHttps = true; o.Providers.Add(); }); // ------------------------- // OpenTelemetry // ------------------------- builder.Services.AddGB5Telemetry(builder.Configuration, "GB5-JobEngine"); // ------------------------- // Assembly Scan for BLL/DAL // ------------------------- var bllAssembly = Assembly.Load("JobEngineBLL"); var dalAssembly = Assembly.Load("JobEngineDAL"); builder.Services.Scan(scan => scan .FromAssemblies(bllAssembly, dalAssembly) .AddClasses(c => c.Where(t => !t.IsAbstract && !t.IsInterface && t.Namespace != null && !t.Namespace.Contains(".DTO") && !typeof(Exception).IsAssignableFrom(t) && t.GetMethod("$") == null)) .AsImplementedInterfaces() .WithScopedLifetime()); // ------------------------- // FastEndpoints // ------------------------- builder.Services.AddFastEndpoints(o => { o.Assemblies = new[] { Assembly.Load("JobEngineSL"), Assembly.Load("JobEngineBLL"), Assembly.Load("JobEngineDAL"), Assembly.Load("GB5Shared") }; }); builder.Services.SwaggerDocument(o => { o.DocumentSettings = s => { s.Title = "GB5 JobEngine API"; s.Version = "v1"; }; o.EnableJWTBearerAuth = false; o.ShortSchemaNames = true; }); builder.Services.Configure(builder.Configuration.GetSection("Gb5SystemDTO")); // ------------------------- // Build App // ------------------------- var app = builder.Build(); // ------------------------- // Startup: load all active Quartz jobs from DB // ------------------------- var quartzSync = app.Services.GetRequiredService(); var startupLog = app.Services.GetRequiredService>(); try { await quartzSync.LoadAllJobsAsync(CancellationToken.None); } catch (Exception startupEx) { startupLog.LogError(startupEx, "JobEngine startup: LoadAllJobsAsync failed — service will start without pre-loaded jobs; Quartz will reload on next DB reconnect"); } // ------------------------- // Middleware // ------------------------- app.UseForwardedHeaders(); app.UseRouting(); app.UseResponseCompression(); app.UseMiddleware(); app.UseMiddleware(); app.UseFastEndpoints(c => { c.Serializer.Options.PropertyNamingPolicy = null; c.Serializer.Options.DictionaryKeyPolicy = null; c.Serializer.Options.AddGB5Converters(); }); app.UseCloudEvents(); app.MapControllers(); app.MapSubscribeHandler(); app.UseOpenApi(); app.UseSwaggerUi(o => { o.Path = "/GB5Documentation"; o.DocumentPath = "/swagger/{documentName}/swagger.json"; o.TransformToExternalPath = (internalUiRoute, _) => "/jobengine" + internalUiRoute; }); app.MapGet("/", () => "Hello from GB5 JobEngine in .NET 9 API!"); // ------------------------- // SignalR Hubs // ------------------------- app.MapHub("/hubs/jobengine"); app.UseAuthorization(); app.Run();