第一阶段迁移已完成。

已实现:
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:
2026-07-22 18:45:54 +08:00 Unverified
parent ecb3dc63ed
commit 241fa8a6d7
20 changed files with 680 additions and 0 deletions
+4
View File
@@ -7,3 +7,7 @@ node_modules/
.env .env
.env.docker .env.docker
*.log *.log
**/bin/
**/obj/
.vs/
artifacts/
+9
View File
@@ -0,0 +1,9 @@
<Project>
<PropertyGroup>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<AnalysisLevel>latest</AnalysisLevel>
</PropertyGroup>
</Project>
+8
View File
@@ -0,0 +1,8 @@
<Solution>
<Folder Name="/src/">
<Project Path="src/Eis.Application/Eis.Application.csproj" />
<Project Path="src/Eis.Domain/Eis.Domain.csproj" />
<Project Path="src/Eis.Infrastructure/Eis.Infrastructure.csproj" />
<Project Path="src/Eis.Web/Eis.Web.csproj" />
</Folder>
</Solution>
+45
View File
@@ -0,0 +1,45 @@
# ASP.NET Core 10 迁移
迁移采用兼容优先的渐进式方案:ASP.NET Core 作为统一入口,尚未迁移的 `/api/*` 请求暂时转发给运行在 `4174` 端口的 Node.js 服务。每完成一个功能域,就在 ASP.NET Core 中注册对应原生端点并停止转发该路径。
## 当前阶段
- [x] ASP.NET Core 10 解决方案与分层项目
- [x] 原前端静态资源无修改托管
- [x] 旧 API 兼容转发,包含 Cookie、请求体、文件下载和状态码
- [x] 存活与迁移就绪检查
- [ ] 公开接口与数据库读取
- [ ] 登录、Session 与 TOTP
- [ ] 考生业务
- [ ] 管理后台、审批流和考务编排
- [ ] 招生录取
- [ ] Excel、文书和缓存
- [ ] 容器入口切换及 Node.js 后端移除
## 本地运行
先在一个终端运行旧 API
```powershell
$env:PORT = '4174'
npm start
```
再在另一个终端运行 ASP.NET Core 入口:
```powershell
dotnet run --project .\src\Eis.Web\Eis.Web.csproj
```
浏览器仍访问 <http://127.0.0.1:4173>。
- `GET /health/live`:只检查 ASP.NET Core 宿主。
- `GET /health/migration`:检查迁移宿主和旧 API 转发链路。
可通过配置 `LegacyNode:Enabled=false` 禁用兼容转发;此时尚未迁移的 API 会返回 `501`
完整的宿主、静态资源、JSON 转发和 Session Cookie 冒烟测试:
```powershell
pwsh.exe -NoLogo -NoProfile -NonInteractive -File .\scripts\smoke-dotnet-migration.ps1
```
+7
View File
@@ -0,0 +1,7 @@
{
"sdk": {
"version": "10.0.302",
"rollForward": "latestPatch",
"allowPrerelease": false
}
}
+182
View File
@@ -0,0 +1,182 @@
[CmdletBinding()]
param()
$ErrorActionPreference = 'Stop'
Set-StrictMode -Version Latest
$repositoryRoot = [System.IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..'))
$temporaryRoot = [System.IO.Path]::GetFullPath([System.IO.Path]::GetTempPath())
$testDirectory = Join-Path $temporaryRoot ("eis-migration-smoke-{0}" -f [guid]::NewGuid().ToString('N'))
$nodeProcess = $null
$dotnetProcess = $null
function Get-AvailableTcpPort {
$listener = [System.Net.Sockets.TcpListener]::new([System.Net.IPAddress]::Loopback, 0)
try {
$listener.Start()
return ([System.Net.IPEndPoint] $listener.LocalEndpoint).Port
}
finally {
$listener.Stop()
}
}
function Start-TestProcess {
param(
[Parameter(Mandatory)]
[string] $FileName,
[Parameter(Mandatory)]
[string[]] $ArgumentList,
[Parameter(Mandatory)]
[hashtable] $Environment
)
$startInfo = [System.Diagnostics.ProcessStartInfo]::new()
$startInfo.FileName = $FileName
$startInfo.WorkingDirectory = $repositoryRoot
$startInfo.UseShellExecute = $false
$startInfo.CreateNoWindow = $true
$startInfo.RedirectStandardOutput = $true
$startInfo.RedirectStandardError = $true
foreach ($argument in $ArgumentList) {
$startInfo.ArgumentList.Add($argument)
}
foreach ($entry in $Environment.GetEnumerator()) {
$startInfo.Environment[$entry.Key] = [string] $entry.Value
}
return [System.Diagnostics.Process]::Start($startInfo)
}
function Wait-ForUrl {
param(
[Parameter(Mandatory)]
[uri] $Uri,
[Parameter(Mandatory)]
[System.Diagnostics.Process[]] $Processes
)
$deadline = [DateTimeOffset]::UtcNow.AddSeconds(25)
while ([DateTimeOffset]::UtcNow -lt $deadline) {
foreach ($process in $Processes) {
if ($process.HasExited) {
$standardOutput = $process.StandardOutput.ReadToEnd()
$standardError = $process.StandardError.ReadToEnd()
throw "Process $($process.Id) exited with code $($process.ExitCode) before $Uri became ready`n$standardOutput`n$standardError"
}
}
try {
$response = Invoke-WebRequest -Uri $Uri -TimeoutSec 2
if ($response.StatusCode -eq 200) {
return
}
}
catch {
Start-Sleep -Milliseconds 250
}
}
throw "Timed out waiting for $Uri"
}
try {
New-Item -ItemType Directory -Path $testDirectory | Out-Null
$legacyPort = Get-AvailableTcpPort
$webPort = Get-AvailableTcpPort
$legacyBaseUrl = "http://127.0.0.1:$legacyPort"
$webBaseUrl = "http://127.0.0.1:$webPort"
$nodeExecutable = (Get-Command node.exe -ErrorAction Stop).Source
$dotnetExecutable = (Get-Command dotnet.exe -ErrorAction Stop).Source
$nodeProcess = Start-TestProcess -FileName $nodeExecutable -ArgumentList @('server.mjs') -Environment @{
NODE_ENV = 'test'
HOST = '127.0.0.1'
PORT = [string] $legacyPort
DATABASE_CLIENT = 'sqlite'
SQLITE_PATH = (Join-Path $testDirectory 'smoke.sqlite')
INITIAL_ADMIN_USERNAME = 'migration_admin'
INITIAL_ADMIN_PASSWORD = 'Migration123!'
INITIAL_ADMIN_DISPLAY_NAME = '迁移测试管理员'
REDIS_URL = ''
REDIS_SESSION_URL = ''
TOTP_ENCRYPTION_KEY = 'migration-smoke-totp-key-32-characters-minimum'
DOCUMENT_VERIFICATION_SECRET = 'migration-smoke-document-key-32-characters-minimum'
}
$projectPath = Join-Path $repositoryRoot 'src\Eis.Web\Eis.Web.csproj'
$dotnetProcess = Start-TestProcess -FileName $dotnetExecutable -ArgumentList @(
'run',
'--project', $projectPath,
'--configuration', 'Release',
'--no-build',
'--no-launch-profile',
'--',
'--urls', $webBaseUrl
) -Environment @{
ASPNETCORE_ENVIRONMENT = 'Development'
LegacyNode__Enabled = 'true'
LegacyNode__BaseUrl = $legacyBaseUrl
}
Wait-ForUrl -Uri "$webBaseUrl/health/live" -Processes @($nodeProcess, $dotnetProcess)
Wait-ForUrl -Uri "$webBaseUrl/health/migration" -Processes @($nodeProcess, $dotnetProcess)
$index = Invoke-WebRequest -Uri "$webBaseUrl/"
if ($index.Content -notmatch '衡准') {
throw 'ASP.NET Core did not serve the existing index.html'
}
$clientModule = Invoke-WebRequest -Uri "$webBaseUrl/src/client/api.mjs"
if ($clientModule.Content -notmatch 'export function api') {
throw 'ASP.NET Core did not serve the existing client module'
}
$homePayload = Invoke-RestMethod -Uri "$webBaseUrl/api/public/home"
if ($homePayload.ok -ne $true) {
throw 'Legacy public API proxy did not preserve the JSON response'
}
$session = [Microsoft.PowerShell.Commands.WebRequestSession]::new()
$loginBody = @{ username = 'migration_admin'; password = 'Migration123!' } | ConvertTo-Json -Compress
$login = Invoke-RestMethod -Uri "$webBaseUrl/api/auth/login" -Method Post -ContentType 'application/json' -Body $loginBody -WebSession $session
if ($login.ok -ne $true -or $login.user.username -ne 'migration_admin') {
throw 'Legacy login API proxy did not preserve the response'
}
$currentUser = Invoke-RestMethod -Uri "$webBaseUrl/api/auth/me" -WebSession $session
if ($currentUser.user.username -ne 'migration_admin') {
throw 'Legacy API proxy did not preserve the session cookie'
}
[pscustomobject]@{
AspNetCoreHost = 'passed'
StaticAssets = 'passed'
JsonProxy = 'passed'
SessionCookie = 'passed'
} | Format-List
}
finally {
foreach ($process in @($dotnetProcess, $nodeProcess)) {
if ($null -ne $process -and -not $process.HasExited) {
$process.Kill($true)
$process.WaitForExit()
}
if ($null -ne $process) {
$process.Dispose()
}
}
if (Test-Path -LiteralPath $testDirectory) {
$resolvedTestDirectory = [System.IO.Path]::GetFullPath($testDirectory)
if (-not $resolvedTestDirectory.StartsWith($temporaryRoot, [StringComparison]::OrdinalIgnoreCase) -or
-not ([System.IO.Path]::GetFileName($resolvedTestDirectory)).StartsWith('eis-migration-smoke-', [StringComparison]::Ordinal)) {
throw "Refusing to remove unexpected smoke-test directory: $resolvedTestDirectory"
}
Remove-Item -LiteralPath $resolvedTestDirectory -Recurse -Force
}
}
@@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<RootNamespace>Eis.Application</RootNamespace>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Eis.Domain\Eis.Domain.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,5 @@
using Eis.Domain.Migration;
namespace Eis.Application.Migration;
public sealed record MigrationFeature(FeatureArea Area, bool Native, string RoutePrefix);
+6
View File
@@ -0,0 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<RootNamespace>Eis.Domain</RootNamespace>
</PropertyGroup>
</Project>
+16
View File
@@ -0,0 +1,16 @@
namespace Eis.Domain.Migration;
/// <summary>
/// Stable feature boundaries used while replacing the legacy implementation.
/// </summary>
public enum FeatureArea
{
Public,
Authentication,
Candidate,
Administration,
Admission,
Documents,
Excel,
Caching
}
@@ -0,0 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<RootNamespace>Eis.Infrastructure</RootNamespace>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Eis.Application\Eis.Application.csproj" />
<ProjectReference Include="..\Eis.Domain\Eis.Domain.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,19 @@
using Eis.Application.Migration;
using Eis.Domain.Migration;
namespace Eis.Infrastructure.Migration;
public static class MigrationFeatureCatalog
{
public static IReadOnlyList<MigrationFeature> Current { get; } =
[
new(FeatureArea.Public, false, "/api/public"),
new(FeatureArea.Authentication, false, "/api/auth"),
new(FeatureArea.Candidate, false, "/api/candidate"),
new(FeatureArea.Administration, false, "/api/admin"),
new(FeatureArea.Admission, false, "/api/admission"),
new(FeatureArea.Documents, false, "/api"),
new(FeatureArea.Excel, false, "/api"),
new(FeatureArea.Caching, false, "/api")
];
}
+22
View File
@@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<RootNamespace>Eis.Web</RootNamespace>
<AssemblyName>Eis.Web</AssemblyName>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Eis.Application\Eis.Application.csproj" />
<ProjectReference Include="..\Eis.Infrastructure\Eis.Infrastructure.csproj" />
</ItemGroup>
<ItemGroup>
<Content Include="..\..\index.html" Link="wwwroot\index.html" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" />
<Content Include="..\..\styles.css" Link="wwwroot\styles.css" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" />
<Content Include="..\..\app.js" Link="wwwroot\app.js" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" />
<Content Include="..\..\src\client\**\*.mjs" Link="wwwroot\src\client\%(RecursiveDir)%(Filename)%(Extension)" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" />
<Content Include="..\..\src\data\china-regions.mjs" Link="wwwroot\src\data\china-regions.mjs" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" />
<Content Include="..\..\src\data\specialty-types.mjs" Link="wwwroot\src\data\specialty-types.mjs" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" />
<Content Include="..\..\node_modules\ckeditor5\dist\browser\ckeditor5.js" Link="wwwroot\vendor\ckeditor5\ckeditor5.js" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" Condition="Exists('..\..\node_modules\ckeditor5\dist\browser\ckeditor5.js')" />
<Content Include="..\..\node_modules\ckeditor5\dist\browser\ckeditor5.css" Link="wwwroot\vendor\ckeditor5\ckeditor5.css" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" Condition="Exists('..\..\node_modules\ckeditor5\dist\browser\ckeditor5.css')" />
<Content Include="..\..\node_modules\ckeditor5\dist\translations\zh-cn.js" Link="wwwroot\vendor\ckeditor5\translations\zh-cn.js" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" Condition="Exists('..\..\node_modules\ckeditor5\dist\translations\zh-cn.js')" />
</ItemGroup>
</Project>
+95
View File
@@ -0,0 +1,95 @@
using Microsoft.Extensions.FileProviders;
using Microsoft.AspNetCore.StaticFiles;
namespace Eis.Web.Frontend;
public static class FrontendAssets
{
private static readonly string[] ClientModules =
[
"api.mjs",
"admin-views.mjs",
"candidate-views.mjs",
"admission-views.mjs",
"admission-plan-editor.mjs",
"public-views.mjs",
"state.mjs",
"ui.mjs",
"table-state.mjs",
"pdf-export.mjs",
"region-select.mjs"
];
public static void MapFrontendAssets(this WebApplication app)
{
var repositoryRoot = FindRepositoryRoot(app.Environment.ContentRootPath);
if (repositoryRoot is null)
{
app.UseDefaultFiles();
app.UseStaticFiles(CreateStaticOptions());
app.MapFallbackToFile("index.html");
return;
}
var clientRoot = Path.Combine(repositoryRoot, "src", "client");
foreach (var fileName in ClientModules)
{
MapFile(app, $"/src/client/{fileName}", Path.Combine(clientRoot, fileName), "text/javascript; charset=utf-8");
}
MapFile(app, "/src/data/china-regions.mjs", Path.Combine(repositoryRoot, "src", "data", "china-regions.mjs"), "text/javascript; charset=utf-8");
MapFile(app, "/src/data/specialty-types.mjs", Path.Combine(repositoryRoot, "src", "data", "specialty-types.mjs"), "text/javascript; charset=utf-8");
MapFile(app, "/styles.css", Path.Combine(repositoryRoot, "styles.css"), "text/css; charset=utf-8");
MapFile(app, "/app.js", Path.Combine(repositoryRoot, "app.js"), "text/javascript; charset=utf-8");
var ckeditorRoot = Path.Combine(repositoryRoot, "node_modules", "ckeditor5", "dist");
if (Directory.Exists(ckeditorRoot))
{
app.UseStaticFiles(new StaticFileOptions
{
FileProvider = new PhysicalFileProvider(Path.Combine(ckeditorRoot, "browser")),
RequestPath = "/vendor/ckeditor5",
OnPrepareResponse = SetNoCache
});
app.UseStaticFiles(new StaticFileOptions
{
FileProvider = new PhysicalFileProvider(Path.Combine(ckeditorRoot, "translations")),
RequestPath = "/vendor/ckeditor5/translations",
OnPrepareResponse = SetNoCache
});
}
var indexPath = Path.Combine(repositoryRoot, "index.html");
MapFile(app, "/", indexPath, "text/html; charset=utf-8");
MapFile(app, "/index.html", indexPath, "text/html; charset=utf-8");
app.MapFallback(async context =>
{
context.Response.ContentType = "text/html; charset=utf-8";
context.Response.Headers.CacheControl = "no-cache";
await context.Response.SendFileAsync(indexPath, context.RequestAborted);
});
}
private static void MapFile(WebApplication app, string route, string path, string contentType) =>
app.MapGet(route, (HttpContext context) =>
{
context.Response.Headers.CacheControl = "no-cache";
return Results.File(path, contentType);
});
private static StaticFileOptions CreateStaticOptions() => new()
{
OnPrepareResponse = SetNoCache
};
private static void SetNoCache(StaticFileResponseContext context) =>
context.Context.Response.Headers.CacheControl = "no-cache";
private static string? FindRepositoryRoot(string contentRoot)
{
var candidate = Path.GetFullPath(Path.Combine(contentRoot, "..", ".."));
return File.Exists(Path.Combine(candidate, "package.json")) && File.Exists(Path.Combine(candidate, "index.html"))
? candidate
: null;
}
}
+135
View File
@@ -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");
}
}
+10
View File
@@ -0,0 +1,10 @@
namespace Eis.Web.Legacy;
public sealed class LegacyNodeOptions
{
public const string SectionName = "LegacyNode";
public bool Enabled { get; init; } = true;
public Uri BaseUrl { get; init; } = new("http://127.0.0.1:4174");
}
+70
View File
@@ -0,0 +1,70 @@
using System.Net;
using Eis.Infrastructure.Migration;
using Eis.Web.Frontend;
using Eis.Web.Legacy;
var builder = WebApplication.CreateBuilder(args);
builder.WebHost.ConfigureKestrel(options => options.AddServerHeader = false);
builder.Services.Configure<LegacyNodeOptions>(builder.Configuration.GetSection(LegacyNodeOptions.SectionName));
builder.Services.AddHttpClient<LegacyApiProxy>((services, client) =>
{
var options = services.GetRequiredService<Microsoft.Extensions.Options.IOptions<LegacyNodeOptions>>().Value;
client.BaseAddress = options.BaseUrl;
client.Timeout = TimeSpan.FromSeconds(30);
client.DefaultRequestHeaders.UserAgent.ParseAdd("Eis.AspNetCore.Migration/1.0");
}).ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler
{
AllowAutoRedirect = false,
AutomaticDecompression = DecompressionMethods.None,
UseCookies = false
});
builder.Services.AddProblemDetails();
var app = builder.Build();
app.UseExceptionHandler();
app.Use(async (context, next) =>
{
context.Response.Headers.XContentTypeOptions = "nosniff";
context.Response.Headers.XFrameOptions = "DENY";
context.Response.Headers["Referrer-Policy"] = "same-origin";
await next();
});
app.MapGet("/health/live", () => Results.Json(new
{
status = "healthy",
service = "Eis.Web",
framework = ".NET 10"
}));
app.MapGet("/health/migration", async (LegacyApiProxy proxy, CancellationToken cancellationToken) =>
{
var legacyAvailable = await proxy.IsAvailableAsync(cancellationToken);
var statusCode = legacyAvailable ? StatusCodes.Status200OK : StatusCodes.Status503ServiceUnavailable;
return Results.Json(new
{
status = legacyAvailable ? "healthy" : "degraded",
legacyApiAvailable = legacyAvailable,
features = MigrationFeatureCatalog.Current
}, statusCode: statusCode);
});
string[] methods =
[
HttpMethods.Get,
HttpMethods.Head,
HttpMethods.Post,
HttpMethods.Put,
HttpMethods.Patch,
HttpMethods.Delete,
HttpMethods.Options
];
app.MapMethods("/api/{**path}", methods, (HttpContext context, LegacyApiProxy proxy) => proxy.ForwardAsync(context));
app.MapFrontendAssets();
app.Run();
public partial class Program;
@@ -0,0 +1,14 @@
{
"$schema": "http://json.schemastore.org/launchsettings.json",
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"applicationUrl": "http://127.0.0.1:4173",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
+13
View File
@@ -0,0 +1,13 @@
{
"LegacyNode": {
"Enabled": true,
"BaseUrl": "http://127.0.0.1:4174"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}
+1
View File
@@ -0,0 +1 @@