// ==========================================================================================
// GameFrameX 组织及其衍生项目的版权、商标、专利及其他相关权利
// GameFrameX organization and its derivative projects' copyrights, trademarks, patents, and related rights
// 均受中华人民共和国及相关国际法律法规保护。
// are protected by the laws of the People's Republic of China and relevant international regulations.
// 使用本项目须严格遵守相应法律法规及开源许可证之规定。
// Usage of this project must strictly comply with applicable laws, regulations, and open-source licenses.
// 本项目采用 Apache License 2.0 单协议分发,
// This project is licensed solely under the Apache License 2.0,
// 完整许可证文本请参见源代码根目录下的 LICENSE 文件。
// please refer to the LICENSE file in the root directory of the source code for the full license text.
// 禁止利用本项目实施任何危害国家安全、破坏社会秩序、
// It is prohibited to use this project to engage in any activities that endanger national security, disrupt social order,
// 侵犯他人合法权益等法律法规所禁止的行为!
// or infringe upon the legitimate rights and interests of others, as prohibited by laws and regulations!
// 因基于本项目二次开发所产生的一切法律纠纷与责任,
// Any legal disputes and liabilities arising from secondary development based on this project
// 本项目组织与贡献者概不承担。
// shall be borne solely by the developer; the project organization and contributors assume no responsibility.
// GitHub 仓库:https://github.com/GameFrameX
// GitHub Repository: https://github.com/GameFrameX
// Gitee 仓库:https://gitee.com/GameFrameX
// Gitee Repository: https://gitee.com/GameFrameX
// CNB 仓库:https://cnb.cool/GameFrameX
// CNB Repository: https://cnb.cool/GameFrameX
// 官方文档:https://gameframex.doc.alianblank.com/
// Official Documentation: https://gameframex.doc.alianblank.com/
// ==========================================================================================
using System.Reflection;
using System.Security.Cryptography.X509Certificates;
using GameFrameX.Foundation.Logger;
using GameFrameX.Foundation.Localization.Core;
using GameFrameX.Network.HTTP;
using GameFrameX.Utility;
using GameFrameX.Foundation.Extensions;
using GameFrameX.Foundation.Utility;
using GameFrameX.Utility.Setting;
using GameFrameX.StartUp.ServiceDefaults;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Server.Kestrel.Core;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.OpenApi;
using Serilog;
namespace GameFrameX.StartUp;
///
/// 程序启动器基类 - 提供服务器的基础功能实现。
///
///
/// Application startup base class - provides server basic functionality implementation.
/// This partial class specifically handles HTTP server startup and configuration functionality.
///
public abstract partial class AppStartUpBase
{
///
/// 当前 HTTP 服务器宿主实例(启动路径保存引用,供停机路径停止与释放; 表示未启动或已停止)。
///
///
/// The current HTTP server host instance (kept by the startup path so the shutdown path can stop and dispose it;
/// when the HTTP server was never started or has already stopped).
/// Mirrors the socket host field _gameServer: assigned right after build and before start, cleared by
/// so repeated shutdown calls stay idempotent. Instance-level (not static) on
/// purpose — every role owns its own AppStartUpBase instance in a multi-role process.
///
private WebApplication _webApplication;
///
/// 启动 HTTP 服务器。
///
///
/// Start HTTP server.
/// This method is responsible for configuring and starting the HTTP server, including Swagger documentation, health checks, exception handling, and other features.
///
/// HTTP 处理器列表,用于处理不同的 HTTP 请求 / HTTP handler list for processing different HTTP requests
/// HTTP 处理器工厂,根据命令标识符创建对应的处理器实例 / HTTP handler factory that creates corresponding handler instances based on command identifiers
/// AOP 处理器列表,用于在 HTTP 请求处理前后执行额外的逻辑 / AOP handler list for executing additional logic before and after HTTP request processing
/// 日志记录的最小级别,用于控制日志输出 / Minimum level for logging to control log output
/// 表示异步操作的任务 / A task representing the asynchronous operation
/// 当 HTTP URL 格式不正确时抛出 / Thrown when HTTP URL format is incorrect
/// 当启用 HTTPS 但未实现时抛出 / Thrown when HTTPS is enabled but not implemented
private async Task StartHttpServerAsync(List baseHandler, Func httpFactory, List aopHandlerTypes = null, LogLevel minimumLevelLogLevel = LogLevel.Debug)
{
// 前置校验:启用状态与 HTTP URL 格式
if (!EnsureHttpServerEnabled())
{
return;
}
// Localization: StartUp.HttpServer.StartingServer - 启动 [HTTP] 服务器...
LogHelper.Info(LocalizationService.GetString(Localization.Keys.StartUp.HttpServer.StartingServer));
if (!Setting.HttpRequireSign)
{
// Localization: StartUp.HttpServer.SignDisabledWarning - HTTP 服务器已启动,但 HttpRequireSign=false,签名校验默认关闭:对外暴露部署存在请求被截获重放的风险,建议生产环境开启签名校验并配置强密钥
LogHelper.Warning(LocalizationService.GetString(Localization.Keys.StartUp.HttpServer.SignDisabledWarning));
}
if (!Setting.HttpPort.IsRange(5000, ushort.MaxValue - 1))
{
// Localization: StartUp.HttpServer.PortOutOfRange - 启动 [HTTP] 服务器端口 [{0}] 超出范围 [{1}-{2}],HTTP服务无法启动,启动已忽略
LogHelper.Warning(LocalizationService.GetString(Localization.Keys.StartUp.HttpServer.PortOutOfRange, Setting.HttpPort, 5000, ushort.MaxValue - 1));
return;
}
// 检查端口是否可用:不可用则记录占用详情后返回
if (!NetHelper.PortIsAvailable(Setting.HttpPort))
{
LogHttpPortOccupied();
return;
}
await RunHttpServerAsync(baseHandler, httpFactory, aopHandlerTypes, minimumLevelLogLevel);
}
///
/// 校验 HTTP 服务器是否启用及 URL 格式是否合法。
///
/// Validate whether the HTTP server is enabled and the URL format is valid. Extracted from to keep cognitive complexity under the Sonar S3776 threshold.
/// 启用且 URL 合法返回 true;未启用返回 false;URL 不合法时抛出 / Returns true if enabled and URL is valid; returns false if disabled; throws for invalid URL
/// 当 HTTP URL 格式不正确时抛出 / Thrown when HTTP URL format is incorrect
private bool EnsureHttpServerEnabled()
{
// 检查是否启用HTTP服务
if (!Setting.IsEnableHttp)
{
// Localization: StartUp.HttpServer.ServiceDisabled - HTTP服务已禁用,启动已忽略
LogHelper.Info(LocalizationService.GetString(Localization.Keys.StartUp.HttpServer.ServiceDisabled));
return false;
}
// 验证HTTP URL格式
if (!Setting.HttpUrl.StartsWith('/'))
{
// Localization: StartUp.HttpExceptions.AddressMustStartWithSlash - 地址必须以斜杠开头。地址:{0}。
throw new ArgumentException(LocalizationService.GetString(Localization.Keys.StartUp.HttpExceptions.AddressMustStartWithSlash, Setting.HttpUrl), nameof(Setting.HttpUrl));
}
if (!Setting.HttpUrl.EndsWith('/'))
{
// Localization: StartUp.HttpExceptions.AddressMustEndWithSlash - 地址必须以斜杠结尾。地址:{0}。
throw new ArgumentException(LocalizationService.GetString(Localization.Keys.StartUp.HttpExceptions.AddressMustEndWithSlash, Setting.HttpUrl), nameof(Setting.HttpUrl));
}
return true;
}
///
/// 记录 HTTP 端口被占用时的占用进程详情。
///
/// Log the occupying process details when the HTTP port is occupied. Extracted from to keep cognitive complexity under the Sonar S3776 threshold.
private void LogHttpPortOccupied()
{
// Localization: StartUp.HttpServer.PortOccupied - HTTP服务器端口被占用。端口:{0}。
LogHelper.Error(LocalizationService.GetString(Localization.Keys.StartUp.HttpServer.PortOccupied, Setting.HttpPort));
var occupiedProcesses = NetHelper.GetPortOccupyingProcesses(Setting.HttpPort);
if (occupiedProcesses.Count > 0)
{
// Localization: StartUp.HttpServer.PortOccupiedDetails - HTTP端口[{0}]占用详情: {1}
LogHelper.Error(LocalizationService.GetString(Localization.Keys.StartUp.HttpServer.PortOccupiedDetails, Setting.HttpPort, string.Join(" | ", occupiedProcesses)));
}
else
{
// Localization: StartUp.HttpServer.PortOccupiedNoProcessDetails - HTTP端口[{0}]已被占用,但未能获取占用进程详情。
LogHelper.Warning(LocalizationService.GetString(Localization.Keys.StartUp.HttpServer.PortOccupiedNoProcessDetails, Setting.HttpPort));
}
}
///
/// 构建、配置并启动 HTTP 服务器。
///
/// Build, configure and start the HTTP server. Extracted from to keep cognitive complexity under the Sonar S3776 threshold.
/// HTTP 处理器列表,用于处理不同的 HTTP 请求 / HTTP handler list for processing different HTTP requests
/// HTTP 处理器工厂,根据命令标识符创建对应的处理器实例 / HTTP handler factory that creates corresponding handler instances based on command identifiers
/// AOP 处理器列表,用于在 HTTP 请求处理前后执行额外的逻辑 / AOP handler list for executing additional logic before and after HTTP request processing
/// 日志记录的最小级别,用于控制日志输出 / Minimum level for logging to control log output
/// 表示异步操作的任务 / A task representing the asynchronous operation
/// 当启用 HTTPS 但未实现时抛出 / Thrown when HTTPS is enabled but not implemented
private async Task RunHttpServerAsync(List baseHandler, Func httpFactory, List aopHandlerTypes, LogLevel minimumLevelLogLevel)
{
var builder = WebApplication.CreateBuilder();
// 确定是否为开发环境
var development = Setting.HttpIsDevelopment || EnvironmentHelper.IsDevelopment();
var openApiInfo = GetOpenApiInfo();
var corsAllowedOrigins = ResolveHttpCorsAllowedOrigins(Setting.HttpCorsAllowedOrigins, development);
ConfigureHttpBuilder(builder, baseHandler, development, openApiInfo, corsAllowedOrigins, minimumLevelLogLevel);
var app = builder.Build();
// 宿主引用落字段(构建后、启动前,对齐 socket 宿主 _gameServer 模式):停机路径 StopHttpServerAsync 依赖它执行 StopAsync/DisposeAsync
_webApplication = app;
app.MapDefaultEndpoints();
ConfigureHttpApplication(app, development, openApiInfo, corsAllowedOrigins, baseHandler, httpFactory, aopHandlerTypes);
await app.StartAsync();
// Localization: StartUp.HttpServer.StartupComplete - HTTP服务器启动完成 - 端口: {0}
LogHelper.Info(LocalizationService.GetString(Localization.Keys.StartUp.HttpServer.StartupComplete, Setting.HttpPort));
}
///
/// 停止并释放 HTTP 服务器宿主(停机时 Kestrel 端口等系统资源的确定性释放)。
///
///
/// Stops and disposes the HTTP server host so Kestrel listeners and other system resources are
/// deterministically released during shutdown. Null-guarded no-op when the HTTP server was never
/// started (disabled or skipped) — those processes keep byte-identical stop behavior. Called by
/// StopServerAsync after the socket host (_gameServer) has stopped, i.e. after the
/// role's actor and business services have already shut down (the Launcher role template stops the
/// capability family before delegating to the base stop flow), so no new shutdown race is introduced.
/// The stop has a hard 5-second budget; the reference is cleared afterwards so repeated shutdown
/// calls stay idempotent.
///
private async Task StopHttpServerAsync()
{
if (_webApplication == null)
{
return;
}
await _webApplication.StopAsync(TimeSpan.FromSeconds(5));
await _webApplication.DisposeAsync();
_webApplication = null;
}
///
/// 配置 HTTP 服务器的 侧服务(CORS、Swagger、Kestrel、日志、OpenTelemetry)。
///
/// Configure the builder-side services of the HTTP server (CORS, Swagger, Kestrel, logging, OpenTelemetry). Extracted from to keep cognitive complexity under the Sonar S3776 threshold.
/// Web 应用程序构建器 / Web application builder
/// HTTP 处理器列表 / HTTP handler list
/// 是否为开发环境 / Whether it is development environment
/// OpenAPI 信息 / OpenAPI information
/// 允许的跨域来源 / Allowed CORS origins
/// 日志记录的最小级别 / Minimum level for logging
private void ConfigureHttpBuilder(WebApplicationBuilder builder, List baseHandler, bool development, OpenApiInfo openApiInfo, string[] corsAllowedOrigins, LogLevel minimumLevelLogLevel)
{
if (corsAllowedOrigins.Length > 0)
{
builder.Services.AddCors(options =>
{
options.AddDefaultPolicy(policy =>
{
policy.WithOrigins(corsAllowedOrigins)
.AllowAnyMethod()
.AllowAnyHeader();
});
});
}
// 在开发环境下配置Swagger
if (development)
{
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(options =>
{
options.SwaggerDoc(openApiInfo.Version, openApiInfo);
options.SchemaFilter();
options.OperationFilter(baseHandler);
options.CustomSchemaIds(type => type.Name);
});
}
// 配置Web主机
var httpsCertificate = Setting.HttpsPort > 0 ? LoadHttpsCertificate(Setting) : null;
var hostBuilder = builder.WebHost.UseKestrel(options =>
{
options.Limits.MaxRequestBodySize = Setting.HttpMaxRequestBodyBytes;
options.ListenAnyIP(Setting.HttpPort);
if (httpsCertificate != null)
{
// HTTPS 真监听:证书在 Kestrel 配置回调外预加载并 fail-fast;
// 端口被占用时由 Kestrel bind 失败直接阻断启动,不再静默跳过(静默降级面移除)。
options.ListenAnyIP(Setting.HttpsPort, listenOptions => listenOptions.UseHttps(httpsCertificate));
}
ConfigureMetricsPortListen(options);
});
hostBuilder.ConfigureLogging(logging =>
{
logging.ClearProviders();
logging.AddSerilog(Log.Logger);
logging.SetMinimumLevel(minimumLevelLogLevel);
});
builder.AddServiceDefaults(Setting.IsOpenTelemetry, Setting.IsOpenTelemetryMetrics, Setting.IsOpenTelemetryTracing);
}
///
/// 为 Kestrel 追加独立指标抓取端口(MetricsPort)监听。
///
///
/// 承载端口契约语义:对外宣称的独立指标抓取端口由主 HTTP 宿主 Kestrel 多绑实现(对齐 HttpsPort 条件监听先例),
/// 不新增第二个 HTTP 服务器组件;仅 OTel 双开关开启且端口未被占用时追加监听,被占用时记 Warning 跳过附加监听、不阻断主服务。
/// Append the standalone metrics scraping port (MetricsPort) listener to the main HTTP host;
/// skipped with a Warning when the port is occupied so the main service is never blocked.
///
/// Kestrel 服务器选项 / Kestrel server options
private void ConfigureMetricsPortListen(KestrelServerOptions options)
{
if (!Setting.IsOpenTelemetry || !Setting.IsOpenTelemetryMetrics || Setting.MetricsPort == 0)
{
return;
}
// 端口被占用:跳过附加监听,/metrics 端点因 RequireHost 端口限定随之失配,主服务继续运行
if (!NetHelper.PortIsAvailable(Setting.MetricsPort))
{
// Localization: StartUp.MetricsPortInUse - 指标端口 [{0}] 被占用,无法启动独立指标服务器
LogHelper.Warning(LocalizationService.GetString(Localization.Keys.StartUp.MetricsPortInUse, Setting.MetricsPort));
return;
}
options.ListenAnyIP(Setting.MetricsPort);
}
///
/// 加载 HTTPS 证书(PFX),供 监听使用;任一配置错误在 Kestrel 监听前 fail-fast。
///
///
/// Loads the HTTPS certificate (PFX) for the listener; any configuration error
/// fails fast before Kestrel starts listening.
/// 调用前提: 的 HttpsPort 大于 0。
///
/// 应用配置 / Application setting
/// 已加载的证书 / The loaded certificate
/// 证书路径为空 / The certificate path is empty
/// 证书文件不存在 / The certificate file does not exist
/// 证书加载失败(密码错误或文件损坏)/ The certificate failed to load (wrong password or corrupted file)
internal static X509Certificate2 LoadHttpsCertificate(AppSetting setting)
{
if (setting.HttpsCertPath.IsNullOrWhiteSpace())
{
// Localization: StartUp.HttpExceptions.HttpsCertPathMissing - 已配置 HTTPS 端口 [{0}],但 HttpsCertPath 未配置(PFX 文件路径),无法启用 HTTPS 监听
throw new InvalidOperationException(LocalizationService.GetString(Localization.Keys.StartUp.HttpExceptions.HttpsCertPathMissing, setting.HttpsPort));
}
if (!File.Exists(setting.HttpsCertPath))
{
// Localization: StartUp.HttpExceptions.HttpsCertFileNotFound - HTTPS 证书文件不存在: {0}
throw new InvalidOperationException(LocalizationService.GetString(Localization.Keys.StartUp.HttpExceptions.HttpsCertFileNotFound, setting.HttpsCertPath));
}
try
{
return X509CertificateLoader.LoadPkcs12FromFile(setting.HttpsCertPath, setting.HttpsCertPassword, X509KeyStorageFlags.DefaultKeySet, Pkcs12LoaderLimits.Defaults);
}
catch (Exception e)
{
// Localization: StartUp.HttpExceptions.HttpsCertLoadFailed - HTTPS 证书加载失败(密码错误或文件损坏): {0}
throw new InvalidOperationException(LocalizationService.GetString(Localization.Keys.StartUp.HttpExceptions.HttpsCertLoadFailed, setting.HttpsCertPath), e);
}
}
///
/// 配置 HTTP 服务器的 请求管道(Swagger UI、CORS、异常处理、Prometheus、路由)。
///
/// Configure the request pipeline of the HTTP server (Swagger UI, CORS, exception handling, Prometheus, routing). Extracted from to keep cognitive complexity under the Sonar S3776 threshold.
/// Web 应用程序实例 / Web application instance
/// 是否为开发环境 / Whether it is development environment
/// OpenAPI 信息 / OpenAPI information
/// 允许的跨域来源 / Allowed CORS origins
/// HTTP 处理器列表 / HTTP handler list
/// HTTP 处理器工厂 / HTTP handler factory
/// AOP 处理器列表 / AOP handler list
private void ConfigureHttpApplication(WebApplication app, bool development, OpenApiInfo openApiInfo, string[] corsAllowedOrigins, List baseHandler, Func httpFactory, List aopHandlerTypes)
{
var ipList = NetHelper.GetLocalIpList();
// 开发环境下的Swagger UI配置
if (development)
{
// 添加 Swagger 中间件
app.UseSwagger();
app.UseSwaggerUI(options =>
{
var swaggerEndpoint = LocalizationService.GetString(Localization.Keys.StartUp.HttpServer.SwaggerEndpointFormat, openApiInfo.Version);
options.SwaggerEndpoint(swaggerEndpoint, openApiInfo.Title);
options.RoutePrefix = LocalizationService.GetString(Localization.Keys.StartUp.HttpServer.SwaggerRoutePrefix);
});
foreach (var ip in ipList)
{
// Localization: StartUp.HttpServer.SwaggerUiAccess - Swagger UI 可通过 http://{0}:{1}/swagger 访问
LogHelper.Debug(LocalizationService.GetString(Localization.Keys.StartUp.HttpServer.SwaggerUiAccess, ip, Setting.HttpPort));
}
}
if (corsAllowedOrigins.Length > 0)
{
app.UseCors();
}
// 配置全局异常处理
app.UseExceptionHandler(ExceptionHandler);
ConfigureHttpPrometheusEndpoint(app, ipList);
RegisterHandlerRoutes(app, baseHandler, httpFactory, aopHandlerTypes, development);
}
///
/// 配置 OpenTelemetry Prometheus 指标端点(内联形态或 MetricsPort 端口限定形态)。
///
/// Configure the OpenTelemetry Prometheus metrics endpoint (inline form or the MetricsPort host-restricted form). Extracted from to keep cognitive complexity under the Sonar S3776 threshold.
/// Web 应用程序实例 / Web application instance
/// 本机 IP 列表 / Local IP list
private void ConfigureHttpPrometheusEndpoint(WebApplication app, List ipList)
{
// 内联形态:未配置独立指标端口时,/metrics 挂主 API 端口
if (Setting.IsOpenTelemetry && Setting.IsOpenTelemetryMetrics && Setting.MetricsPort == 0)
{
app.MapPrometheusScrapingEndpoint();
foreach (var ip in ipList)
{
// Localization: StartUp.PrometheusMetricsEndpointEnabledInline - Prometheus指标端点已内联启用。
LogHelper.Info(LocalizationService.GetString(Localization.Keys.StartUp.PrometheusMetricsEndpointEnabledInline, ip, Setting.HttpPort));
}
}
else if (Setting.IsOpenTelemetry && Setting.IsOpenTelemetryMetrics && Setting.MetricsPort > 0)
{
// 端口契约:独立指标抓取端口由主 HTTP 宿主 Kestrel 多绑承载(ConfigureMetricsPortListen 追加监听),
// /metrics 以 RequireHost("*:MetricsPort") 限定仅指标端口应答,主 API 端口不暴露指标面。
// Port contract: /metrics only answers on MetricsPort (RequireHost host:port matching); the main API port does not expose it.
app.MapPrometheusScrapingEndpoint().RequireHost($"*:{Setting.MetricsPort}");
foreach (var ip in ipList)
{
// Localization: StartUp.PrometheusMetricsEndpointEnabled - 独立普罗米修斯指标端点已启用: http://{0}:{1}/metrics
LogHelper.Info(LocalizationService.GetString(Localization.Keys.StartUp.PrometheusMetricsEndpointEnabled, ip, Setting.MetricsPort));
}
}
}
///
/// 注册全部 HTTP 处理器路由(驼峰与下划线两种格式)。
///
/// Register routes for all HTTP handlers (both camelCase and underscore forms). Extracted from to keep cognitive complexity under the Sonar S3776 threshold.
/// Web 应用程序实例 / Web application instance
/// HTTP 处理器列表 / HTTP handler list
/// HTTP 处理器工厂 / HTTP handler factory
/// AOP 处理器列表 / AOP handler list
/// 是否为开发环境 / Whether it is development environment
private static void RegisterHandlerRoutes(WebApplication app, List baseHandler, Func httpFactory, List aopHandlerTypes, bool development)
{
var registeredRouteKeys = new HashSet(StringComparer.OrdinalIgnoreCase);
foreach (var handler in baseHandler)
{
var handlerType = handler.GetType();
var mappingAttribute = handlerType.GetCustomAttribute();
if (mappingAttribute == null)
{
continue;
}
// 注册驼峰格式路由
RegisterHandlerRoute(app, mappingAttribute, mappingAttribute.OriginalCmd, httpFactory, aopHandlerTypes, development, registeredRouteKeys);
// 注册下划线格式路由(如果与驼峰格式不同)
if (mappingAttribute.OriginalCmd != mappingAttribute.StandardCmd)
{
RegisterHandlerRoute(app, mappingAttribute, mappingAttribute.StandardCmd, httpFactory, aopHandlerTypes, development, registeredRouteKeys);
}
}
}
///
/// 获取或创建 Swagger 信息。
///
///
/// Get or create Swagger information.
/// Used to configure basic information for Swagger documentation, including title, version, contact information, etc.
///
/// 包含 API 文档详细信息的 OpenAPI 信息对象 / OpenAPI information object containing API documentation details
private OpenApiInfo GetOpenApiInfo()
{
// 添加 Swagger 服务
var version = Assembly.GetExecutingAssembly().GetName().Version;
if (version == null)
{
version = new Version(1, 0, 0);
}
var openApiInfo = new OpenApiInfo
{
Title = LocalizationService.GetString(Localization.Keys.StartUp.HttpServer.ApiTitle),
Version = $"v{version.Major}.{version.Minor}",
TermsOfService = new Uri("https://gameframex.doc.alianblank.com"),
Contact = new OpenApiContact { Url = new Uri("https://gameframex.doc.alianblank.com"), Name = LocalizationService.GetString(Localization.Keys.StartUp.HttpServer.ApiContactName), Email = "wangfj11@foxmail.com", },
License = new OpenApiLicense { Name = LocalizationService.GetString(Localization.Keys.StartUp.HttpServer.ApiLicenseName), Url = new Uri("https://github.com/GameFrameX/GameFrameX"), },
Description = LocalizationService.GetString(Localization.Keys.StartUp.HttpServer.ApiDescription),
};
return openApiInfo;
}
private static string[] ResolveHttpCorsAllowedOrigins(string allowedOrigins, bool development)
{
if (!string.IsNullOrWhiteSpace(allowedOrigins))
{
return allowedOrigins.Split(new[] { ',', ';' }, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToArray();
}
if (!development)
{
return Array.Empty();
}
return new[]
{
"http://localhost",
"https://localhost",
"http://127.0.0.1",
"https://127.0.0.1",
};
}
///
/// 异常处理器,用于处理 HTTP 请求中的未处理异常。
///
///
/// Exception handler for processing unhandled exceptions in HTTP requests.
/// Provides a global exception handling mechanism to ensure all unhandled exceptions are properly handled and returned to the client.
///
/// 用于配置错误处理管道的应用程序构建器 / Application builder for configuring error handling pipeline
private static void ExceptionHandler(IApplicationBuilder errorContext)
{
errorContext.Run(async context =>
{
// 获取异常信息
var exceptionHandlerPathFeature = context.Features.Get();
// 记录详细错误日志
// Localization: StartUp.HttpServer.HttpRequestError - HTTP请求错误: {0}
LogHelper.Error(LocalizationService.GetString(Localization.Keys.StartUp.HttpServer.HttpRequestError, exceptionHandlerPathFeature?.Error.ToString() ?? "No exception available"));
// 返回通用错误消息,避免泄露敏感信息
await context.Response.WriteAsync("An error occurred while processing your request.");
});
}
///
/// 获取 HTTP 方法的摘要描述。
///
///
/// Gets the summary description for the HTTP method.
///
/// HTTP 方法类型 / HTTP method type
/// 摘要描述 / Summary description
private static string GetHttpMethodSummary(HttpMethodType httpMethod)
{
switch (httpMethod)
{
case HttpMethodType.GET:
return LocalizationService.GetString(Localization.Keys.StartUp.HttpServer.HandleGetRequest);
case HttpMethodType.PUT:
return LocalizationService.GetString(Localization.Keys.StartUp.HttpServer.HandlePutRequest);
case HttpMethodType.DELETE:
return LocalizationService.GetString(Localization.Keys.StartUp.HttpServer.HandleDeleteRequest);
default:
return LocalizationService.GetString(Localization.Keys.StartUp.HttpServer.HandlePostRequest);
}
}
///
/// 获取 HTTP 方法的详细描述。
///
///
/// Gets the detailed description for the HTTP method.
///
/// HTTP 方法类型 / HTTP method type
/// 详细描述 / Detailed description
private static string GetHttpMethodDescription(HttpMethodType httpMethod)
{
switch (httpMethod)
{
case HttpMethodType.GET:
return LocalizationService.GetString(Localization.Keys.StartUp.HttpServer.HandleGameClientGetRequest);
case HttpMethodType.PUT:
return LocalizationService.GetString(Localization.Keys.StartUp.HttpServer.HandleGameClientPutRequest);
case HttpMethodType.DELETE:
return LocalizationService.GetString(Localization.Keys.StartUp.HttpServer.HandleGameClientDeleteRequest);
default:
return LocalizationService.GetString(Localization.Keys.StartUp.HttpServer.HandleGameClientPostRequest);
}
}
///
/// 注册单个 HTTP 处理器路由。
///
///
/// Registers a single HTTP handler route.
/// Registers the route based on the HTTP method - using different Map methods according to HttpMethod.
/// In development environment, configures API documentation.
///
/// Web 应用程序实例 / Web application instance
/// HTTP 消息映射特性 / HTTP message mapping attribute
/// 命令标识符(用于构建 API 路径)/ Command identifier (used to build API path)
/// HTTP 处理器工厂 / HTTP handler factory
/// AOP 处理器列表 / AOP handler list
/// 是否为开发环境 / Whether it is development environment
/// 已注册路由键集合,用于去重 / The set of registered route keys, used for deduplication
private static void RegisterHandlerRoute(WebApplication app, HttpMessageMappingAttribute mappingAttribute, string cmd, Func httpFactory, List aopHandlerTypes, bool development, ISet registeredRouteKeys)
{
var apiPath = $"{GlobalSettings.CurrentSetting.HttpUrl}{cmd}";
var httpMethod = mappingAttribute.HttpMethod;
if (!TryAddRouteKey(httpMethod, apiPath, registeredRouteKeys))
{
return;
}
IEndpointConventionBuilder route;
switch (httpMethod)
{
case HttpMethodType.GET:
route = app.MapGet(apiPath, async (HttpContext context) => { await HttpHandler.HandleRequestAsync(context, httpFactory, aopHandlerTypes); });
break;
case HttpMethodType.PUT:
route = app.MapPut(apiPath, async (HttpContext context, string _) => { await HttpHandler.HandleRequestAsync(context, httpFactory, aopHandlerTypes); });
break;
case HttpMethodType.DELETE:
route = app.MapDelete(apiPath, async (HttpContext context, string _) => { await HttpHandler.HandleRequestAsync(context, httpFactory, aopHandlerTypes); });
break;
default:
route = app.MapPost(apiPath, async (HttpContext context, string _) => { await HttpHandler.HandleRequestAsync(context, httpFactory, aopHandlerTypes); });
break;
}
// 开发环境下配置API文档
if (development)
{
route.WithSummary(GetHttpMethodSummary(httpMethod));
route.WithDescription(GetHttpMethodDescription(httpMethod));
}
}
///
/// 尝试添加路由唯一键,避免大小写差异导致重复注册。
///
/// HTTP方法
/// API路径
/// 已注册路由键集合
/// 添加成功返回 true,已存在返回 false
private static bool TryAddRouteKey(HttpMethodType httpMethod, string apiPath, ISet registeredRouteKeys)
{
var routeKey = $"{httpMethod}:{apiPath}";
return registeredRouteKeys.Add(routeKey);
}
}