已实现: ASP.NET Core 10 分层解决方案:[Eis.slnx](C:/Users/BI/Documents/EIS-dotnet/Eis.slnx) ASP.NET Core 统一入口与健康检查:[Program.cs](C:/Users/BI/Documents/EIS-dotnet/src/Eis.Web/Program.cs) 原前端静态资源无修改托管 未迁移 API 自动转发至旧 Node 服务,兼容 JSON、Cookie、请求体和文件下载 可重复执行的端到端测试:[smoke-dotnet-migration.ps1](C:/Users/BI/Documents/EIS-dotnet/scripts/smoke-dotnet-migration.ps1) 迁移进度与运行说明:[MIGRATION.md](C:/Users/BI/Documents/EIS-dotnet/MIGRATION.md) 验证结果:
136 lines
4.5 KiB
C#
136 lines
4.5 KiB
C#
using System.Net;
|
|
using System.Net.Http.Headers;
|
|
using Microsoft.Extensions.Options;
|
|
|
|
namespace Eis.Web.Legacy;
|
|
|
|
public sealed class LegacyApiProxy(
|
|
HttpClient httpClient,
|
|
IOptions<LegacyNodeOptions> options,
|
|
ILogger<LegacyApiProxy> logger)
|
|
{
|
|
private static readonly HashSet<string> HopByHopHeaders = new(StringComparer.OrdinalIgnoreCase)
|
|
{
|
|
"Connection",
|
|
"Keep-Alive",
|
|
"Proxy-Authenticate",
|
|
"Proxy-Authorization",
|
|
"TE",
|
|
"Trailer",
|
|
"Transfer-Encoding",
|
|
"Upgrade"
|
|
};
|
|
|
|
private readonly LegacyNodeOptions _options = options.Value;
|
|
|
|
public async Task ForwardAsync(HttpContext context)
|
|
{
|
|
if (!_options.Enabled)
|
|
{
|
|
context.Response.StatusCode = StatusCodes.Status501NotImplemented;
|
|
await context.Response.WriteAsJsonAsync(new
|
|
{
|
|
ok = false,
|
|
message = "该接口尚未迁移到 ASP.NET Core",
|
|
migration = new { native = false, legacyProxyEnabled = false }
|
|
}, context.RequestAborted);
|
|
return;
|
|
}
|
|
|
|
var target = new Uri(_options.BaseUrl, $"{context.Request.PathBase}{context.Request.Path}{context.Request.QueryString}");
|
|
using var outbound = CreateRequest(context, target);
|
|
|
|
try
|
|
{
|
|
using var upstream = await httpClient.SendAsync(
|
|
outbound,
|
|
HttpCompletionOption.ResponseHeadersRead,
|
|
context.RequestAborted);
|
|
|
|
context.Response.StatusCode = (int)upstream.StatusCode;
|
|
CopyResponseHeaders(upstream, context.Response);
|
|
|
|
if (context.Request.Method != HttpMethods.Head && upstream.StatusCode != HttpStatusCode.NoContent)
|
|
{
|
|
await upstream.Content.CopyToAsync(context.Response.Body, context.RequestAborted);
|
|
}
|
|
}
|
|
catch (HttpRequestException exception)
|
|
{
|
|
logger.LogWarning(exception, "Legacy Node API at {Target} is unavailable", target);
|
|
if (context.Response.HasStarted)
|
|
{
|
|
context.Abort();
|
|
return;
|
|
}
|
|
|
|
context.Response.StatusCode = StatusCodes.Status503ServiceUnavailable;
|
|
await context.Response.WriteAsJsonAsync(new
|
|
{
|
|
ok = false,
|
|
message = "迁移期间的旧版 API 服务暂时不可用",
|
|
migration = new { native = false, legacyProxyEnabled = true }
|
|
}, context.RequestAborted);
|
|
}
|
|
}
|
|
|
|
public async Task<bool> IsAvailableAsync(CancellationToken cancellationToken)
|
|
{
|
|
if (!_options.Enabled)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
try
|
|
{
|
|
using var response = await httpClient.GetAsync("api/public/home", cancellationToken);
|
|
return response.IsSuccessStatusCode;
|
|
}
|
|
catch (HttpRequestException)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private static HttpRequestMessage CreateRequest(HttpContext context, Uri target)
|
|
{
|
|
var request = new HttpRequestMessage(new HttpMethod(context.Request.Method), target);
|
|
var hasBody = context.Request.ContentLength > 0 || context.Request.Headers.TransferEncoding.Count > 0;
|
|
if (hasBody)
|
|
{
|
|
request.Content = new StreamContent(context.Request.Body);
|
|
}
|
|
|
|
foreach (var (name, values) in context.Request.Headers)
|
|
{
|
|
if (name.Equals("Host", StringComparison.OrdinalIgnoreCase) || HopByHopHeaders.Contains(name))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var valueArray = values.ToArray();
|
|
if (!request.Headers.TryAddWithoutValidation(name, valueArray) && request.Content is not null)
|
|
{
|
|
request.Content.Headers.TryAddWithoutValidation(name, valueArray);
|
|
}
|
|
}
|
|
|
|
request.Headers.TryAddWithoutValidation("X-Forwarded-Host", context.Request.Host.Value);
|
|
request.Headers.TryAddWithoutValidation("X-Forwarded-Proto", context.Request.Scheme);
|
|
return request;
|
|
}
|
|
|
|
private static void CopyResponseHeaders(HttpResponseMessage upstream, HttpResponse response)
|
|
{
|
|
foreach (var header in upstream.Headers.Concat(upstream.Content.Headers))
|
|
{
|
|
if (!HopByHopHeaders.Contains(header.Key))
|
|
{
|
|
response.Headers.Append(header.Key, header.Value.ToArray());
|
|
}
|
|
}
|
|
|
|
response.Headers.Remove("transfer-encoding");
|
|
}
|
|
}
|