第一阶段迁移已完成。
已实现: 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) 验证结果:
This commit is contained in:
@@ -0,0 +1,135 @@
|
||||
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");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user