PAYATHON 2026

Visual Studio 2026 多启动项目中 Web.Api 无法访问

排障赵师傅

结论

SocketException: actively refused 说明 Web.UI 发起请求时,localhost:7124 上没有进程监听。异常发生在 TCP 连接阶段,与 HTTPS 开发证书、IHttpClientFactory 或 JwtDelegatingHandler 本身无关。

Visual Studio 的多项目启动会同时发起各项目的启动操作,但无法保证以下几件事:

  • 各项目使用了预期的 launchSettings.json profile;
  • Web.Api 绑定端口后,Web.UI 才开始启动;
  • Web.Api 通过健康检查后,Web.UI 才处理第一个请求。

需要分别解决启动配置和服务就绪两个问题:

  1. 显式指定 Web.Api 使用 https profile,确认程序进入了 Program.Main 并成功绑定端口。
  2. Web.UI 调用 Web.Api 时增加就绪等待或重试,避免并发启动引发竞态。

只调整项目的排列顺序,无法保证 API 已经可用。现有信息也不足以判断 Web.Api 控制台持续为空是 Visual Studio 18.6.2 的已知缺陷。更可能的原因是多项目启动配置、调试目标,或 .vs 中保存的用户级启动状态有误。

确认 Visual Studio 实际启动的项目

在解决方案属性的 Startup Project 页面中选择 Multiple startup projects,检查以下设置:

  • Web.Api 的 Action 为 Start;
  • Web.UI 的 Action 为 Start;
  • Web.Api 的调试目标为 https,没有选成 IIS Express、WSL、Docker 或其他 profile;
  • 当前 Solution Configuration 中,两个项目都勾选了 Build;
  • Web.Api 没有停在入口断点、异常断点或模块加载阶段。

按下 F5 后,打开 Visual Studio 的 Debug > Windows > Processes,检查是否存在 Web.Api 对应的进程。不要只凭 console window 判断项目是否已经运行。调试启动器可能已经创建窗口,但目标程序还没有进入应用入口,也可能在绑定端口前就退出了。

还要查看 Output 窗口中的以下输出源:

  • Debug
  • Build
  • ASP.NET Core Web Server

如果 Web.Api 进程立即退出,退出代码和启动错误通常会出现在这些输出中,不一定会写入项目的 console。

也可以在 Program.cs 最前面临时加入一条不依赖 Serilog 的输出:

Console.Error.WriteLine(
    $"Web.Api entering Program.cs, PID={Environment.ProcessId}");

如果这条输出没有出现,说明问题发生在应用代码执行之前,应继续检查 Visual Studio 的启动目标和调试配置。如果输出出现了,却没有 Now listening on,再检查 builder.Build()、Serilog 初始化、依赖注入,以及启动时执行的数据库或网络操作。

显式使用 https launch profile

Web.Api 的 launchSettings.json 可以保留以下配置:

{
  "$schema": "http://json.schemastore.org/launchsettings.json",
  "profiles": {
    "https": {
      "commandName": "Project",
      "dotnetRunMessages": true,
      "launchBrowser": false,
      "applicationUrl": "https://localhost:7124;http://localhost:5247",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    }
  }
}

端口绑定不依赖 dotnetRunMessages,但启用它后,可以更直观地确认 Kestrel 的监听地址。

Web.UI 也应配置一个名称明确的 profile,例如:

{
  "$schema": "http://json.schemastore.org/launchsettings.json",
  "profiles": {
    "https": {
      "commandName": "Project",
      "dotnetRunMessages": true,
      "launchBrowser": true,
      "applicationUrl": "https://localhost:7063;http://localhost:5063",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    }
  }
}

修改后,在 Visual Studio 工具栏中分别选中两个项目,确认它们都能单独使用名为 https 的目标启动。

使用 .slnLaunch 保存多项目启动配置

较新的 Visual Studio 可以把多项目启动设置保存到解决方案级的 .slnLaunch 文件中。建议先在 Visual Studio 的多项目启动配置界面中创建并保存配置,再检查生成的内容,以免手写了当前版本不支持的字段。

典型配置如下:

[
  {
    "Name": "Web.Api + Web.UI",
    "Projects": [
      {
        "Path": "Web.Api\\Web.Api.csproj",
        "Action": "Start",
        "DebugTarget": "https"
      },
      {
        "Path": "Web.UI\\Web.UI.csproj",
        "Action": "Start",
        "DebugTarget": "https"
      }
    ]
  }
]

项目路径需要根据实际的解决方案目录调整。DebugTarget 用于选择 launchSettings.json 中的 profile。如果 Visual Studio 18.6.2 通过界面生成的文件没有这个字段,或者采用了不同的字段结构,应以该版本实际生成的配置为准,不要直接照搬示例。

.slnLaunch 负责指定启动哪些项目以及使用哪个调试目标。它通常不会等到 Web.Api 的 /health 返回成功后才启动 Web.UI,因此还要处理服务就绪竞态。

等待 Web.Api 就绪后再发起请求

即使 Web.Api 已被正确启动,两个项目仍会并发初始化。Web.UI 可能已经启动并开始访问 API,而 Web.Api 还在加载配置、执行迁移或初始化 Serilog。

可以为健康检查配置单独的客户端:

builder.Services.AddHttpClient("WebApiHealth", client =>
{
    client.BaseAddress = new Uri(
        builder.Configuration["WebApi:BaseUrl"]
        ?? throw new InvalidOperationException(
            "Configuration 'WebApi:BaseUrl' is missing."));
    client.Timeout = TimeSpan.FromSeconds(2);
});

在 Web.UI 开始处理请求前等待 API:

var app = builder.Build();

var clientFactory =
    app.Services.GetRequiredService<IHttpClientFactory>();

var healthClient = clientFactory.CreateClient("WebApiHealth");

const int maxAttempts = 10;
var apiReady = false;

for (var attempt = 1; attempt <= maxAttempts; attempt++)
{
    try
    {
        using var response =
            await healthClient.GetAsync("/health");

        if (response.IsSuccessStatusCode)
        {
            apiReady = true;
            break;
        }
    }
    catch (HttpRequestException) when (attempt < maxAttempts)
    {
        // API 进程可能尚未绑定端口。
    }
    catch (TaskCanceledException) when (attempt < maxAttempts)
    {
        // 单次健康检查超时,继续尝试。
    }

    await Task.Delay(TimeSpan.FromSeconds(1));
}

if (!apiReady)
{
    throw new InvalidOperationException(
        $"Web.Api did not become ready after {maxAttempts} attempts.");
}

await app.RunAsync();

如果 Web.UI 可以在 API 暂时不可用时继续运行,就不必抛出异常,可以改为记录错误并返回 503 Service Unavailable。选择哪种策略,取决于 Web.UI 是否允许降级运行。

业务请求也应设置有限次数的重试。不要无限重试,也不要重试明显无法自行恢复的 4xx 响应。

重建 Visual Studio 的用户级启动状态

如果 Web.Api 可以在 Visual Studio 中单独正常启动,但使用旧的多项目配置时总是出现空 console,可以重建 Visual Studio 保存的解决方案状态:

  1. 关闭 Visual Studio。
  2. 将解决方案目录下的 .vs 文件夹重命名,例如 .vs.backup。
  3. 重新打开解决方案。
  4. 重新创建多项目启动配置,并为两个项目明确选择 https profile。
  5. 执行 Rebuild Solution,然后再次启动。

重命名后仍可在需要时恢复,比直接删除更稳妥。.vs 保存的是本机用户级状态,通常不包含项目源代码,但可能包含启动目标、调试设置和解决方案缓存。

如果问题仍能稳定复现,可以继续尝试:

  • 将 Visual Studio 更新到当前可用的修复版本;
  • 暂时禁用可能影响调试启动的扩展;
  • 使用 Start Without Debugging 对 Web.Api 进行对照测试;
  • 收集 Visual Studio ActivityLog.xml、Output 窗口内容、目标进程退出代码和最小复现解决方案,然后提交反馈。

验证问题是否解决

启动多项目配置后,运行:

netstat -ano | findstr :7124

应当能看到监听记录。再运行:

curl.exe -k https://localhost:7124/health

应当得到成功响应。

-k 只适合本地诊断,用于排除客户端证书信任问题,不应写入正式应用代码。Web.UI 仍应通过受信任的开发证书正常访问 HTTPS。

如果 netstat 没有监听记录,应继续排查 Web.Api 的启动过程。如果端口已经处于监听状态,但 Web.UI 仍然请求失败,再检查 WebApi:BaseUrl 的环境覆盖、代理设置、IPv4/IPv6 解析以及 JwtDelegatingHandler。这两类故障需要分开排查。

备注:内容仅供参考。