You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

ASP.NET Core 8.0 Web API gRPC集成测试连接失败问题

ASP.NET Core 8.0 gRPC集成测试连接拒绝问题排查

问题背景

我用ASP.NET Core 8.0开发了一个Web API,已配置支持Http1和Http2:

"Kestrel": {
  "EndpointDefaults": {
    "Protocols": "Http1AndHttp2"
  }
}

普通HTTP调用正常,但gRPC调用始终报连接拒绝错误。生产环境中API运行状态良好,HTTP和gRPC均能正常工作,使用protobuf-net库实现代码优先gRPC。核心问题是集成测试的夹具配置存在问题,导致无法正常发起gRPC调用。

相关代码

Program.cs

var builder = WebApplication.CreateBuilder(args);

ServiceRegistration<AppSettings>.ConfigureServices(builder, "AdcTrains", "adc-trains",
    containerBuilderAdditionalActions:
        containerBuilder =>
        {
        },
    additionalMiddlewareActions: webApp =>
    {
        if (webApp.Environment.IsDevelopment())
        {
            webApp.MapCodeFirstGrpcReflectionService();
        }

        webApp.MapGrpcService<TrainGrpcService>();
    },
    additionalPostBuildBehavior: async webApp =>
    {
        // 后续用集成测试替代此处逻辑
        var tenantIdString = "19d74b3c-2955-4667-9bc5-15d67212469e";

        var multitenantService = webApp.Services.GetRequiredService<MultitenantContainer>();
        var tenantScope = multitenantService.GetTenantScope(tenantIdString);
        var resolverService = tenantScope.Resolve<IGrpcResolverService>();
        var trainGrpcService = resolverService.ResolveService<ITrainGrpcService>(Guid.Parse(tenantIdString));
    });

/// <summary>
/// 请勿删除,供测试项目使用
/// </summary>
public partial class Program 
{ }

基础库中启用代码优先gRPC的代码

// 实际暴露gRPC端点
services.AddCodeFirstGrpcReflection();

services.AddCodeFirstGrpc(config =>
  {
      config.ResponseCompressionLevel = System.IO.Compression.CompressionLevel.Optimal;
  });

集成测试的CustomWebApplicationFactory

/// <summary>
/// 所有集成测试的基础夹具
/// </summary>
public class CustomWebApplicationFactory<TProgram> : WebApplicationFactory<TProgram>, IAsyncLifetime where TProgram : class
{
    protected const string TenantId = "19d74b3c-2955-4667-9bc5-15d67212469e";
    // 在InitializeAsync中初始化
    public HttpClient HttpClient = default!;

    public HubConnection WebHubConnection { get; set; }
    public HubConnection RequestHubConnection { get; set; }

    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        builder.UseEnvironment("Test");

        builder
            //.UseTestServer()
            .ConfigureServices(services =>
            {
                services.AddCodeFirstGrpcReflection();

                services.AddCodeFirstGrpc(config =>
                {
                    config.ResponseCompressionLevel = System.IO.Compression.CompressionLevel.Optimal;
                    config.EnableDetailedErrors = true;
                });
            })
            .UseUrls("https://localhost:7034")
            .ConfigureKestrel(k => 
            {
                k.ConfigureEndpointDefaults(lo => lo.Protocols = HttpProtocols.Http1AndHttp2);
                k.ListenAnyIP(7034, listenOptions =>
                {
                    listenOptions.Protocols = HttpProtocols.Http1AndHttp2;
                });
            })
            .Configure(app =>
            {
                app.UseRouting();

                app.UseEndpoints(endpoints =>
                {
                    endpoints.MapGrpcService<TrainGrpcService>();
                });
            }); 
    }

    #region IAsyncLifetime
    public async Task InitializeAsync()
    {
        HttpClient = CreateClient();

        var configuration = Services.GetRequiredService<IConfiguration>();
        var testConfiguration = configuration.GetSection("TestConfiguration");

        var httpClient = new HttpClient();

        var tokenResponse = await httpClient.RequestPasswordTokenAsync(new PasswordTokenRequest
        {
            Address = testConfiguration.GetValue<string>("Ids"),
            ClientId = testConfiguration.GetValue<string>("ClientId")!,
            Scope = testConfiguration.GetValue<string>("Scope"),

            UserName = testConfiguration.GetValue<string>("UserName")!,
            Password = testConfiguration.GetValue<string>("Password")!
        });

        HttpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", tokenResponse.AccessToken);
        HttpClient.DefaultRequestHeaders.Add("tenantId", TenantId);

        // SignalR连接初始化
        var hubConnectionService = new HubConnectionService(configuration);

        var cancellationTokenSource = new CancellationTokenSource(15000);

        // 前端通知连接
        WebHubConnection = await hubConnectionService.HubInitialize("webNotifications", tokenResponse.AccessToken, cancellationTokenSource.Token);

        // 模拟OBS系统连接
        RequestHubConnection = await hubConnectionService.HubInitialize("trainRequest", tokenResponse.AccessToken, cancellationTokenSource.Token);

        if (WebHubConnection.ConnectionId is null)
            throw new Exception("SignalR连接失败");

        Log.Information("Hub connectionId: {hubConnectionId}", WebHubConnection.ConnectionId);
    }

    /// <summary>
    /// 登出
    /// </summary>
    /// <returns></returns>
    public async Task DisposeAsync()
    {
        var configuration = Services.GetRequiredService<IConfiguration>();
        var testConfiguration = configuration.GetSection("TestConfiguration");

        await HttpClient.RevokeTokenAsync(new TokenRevocationRequest
        {
            Token = HttpClient.DefaultRequestHeaders.Authorization!.Parameter,
            Address = testConfiguration.GetValue<string>("Ids")
        });
    }
    #endregion
}

测试夹具

public class TrainGrpcConfigFixture : IClassFixture<CustomWebApplicationFactory<Program>>
{
    private readonly CustomWebApplicationFactory<Program> _factory;

    public TrainGrpcConfigFixture(CustomWebApplicationFactory<Program> factory)
        => _factory = factory;

    [Fact]
    public async Task Get_NvrSet_Response_ExpectData()
    {
        // 准备
        var grpcResolver = _factory.Services.GetService(typeof(IGrpcResolverService)) as IGrpcResolverService;
        var trainGrpcService = grpcResolver.ResolveService<ITrainGrpcService>();
        var response = await trainGrpcService.GetNvrSetResponseAsync(new TrainConfig.Grpc.Models.Dtos.NvrSet.Requests.NvrSetRequest() { TrainId = 8 });
    }
}

问题分析与解决方案

核心问题点

  1. 测试服务器HTTP2配置缺失:你注释掉了.UseTestServer(),手动指定端口后,Kestrel缺少HTTPS证书配置,导致HTTPS端点无法正常启动,gRPC依赖HTTP2,因此无法建立连接。
  2. 服务调用方式错误:测试中直接从容器解析ITrainGrpcService,若期望的是网络调用,这种方式属于直接调用服务实现,而非通过gRPC协议发起网络请求;若要直接调用,需确保租户上下文正确注入。

解决方案

方案1:使用TestServer进行gRPC集成测试(推荐)

TestServer原生支持HTTP2,无需手动配置端口和证书,修改CustomWebApplicationFactory:

protected override void ConfigureWebHost(IWebHostBuilder builder)
{
    builder.UseEnvironment("Test");

    // 启用TestServer,默认支持HTTP2
    builder.UseTestServer()
           .ConfigureServices(services =>
           {
               services.AddCodeFirstGrpc(config =>
               {
                   config.ResponseCompressionLevel = System.IO.Compression.CompressionLevel.Optimal;
                   config.EnableDetailedErrors = true;
               });
           })
           .Configure(app =>
           {
               app.UseRouting();
               app.UseEndpoints(endpoints =>
               {
                   endpoints.MapGrpcService<TrainGrpcService>();
                   // 测试环境也启用反射服务,方便调试
                   endpoints.MapCodeFirstGrpcReflectionService();
               });
           });
}

在测试中创建gRPC客户端发起网络调用:

[Fact]
public async Task Get_NvrSet_Response_ExpectData()
{
    // 创建TestServer对应的HTTP客户端
    var httpClient = _factory.CreateDefaultClient();
    // 配置gRPC通道,忽略测试证书验证
    var channel = GrpcChannel.ForAddress(httpClient.BaseAddress, new GrpcChannelOptions
    {
        HttpClient = httpClient,
        HttpHandler = new HttpClientHandler
        {
            ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator
        }
    });

    // 实例化gRPC客户端(根据你的protobuf-net生成的客户端类型调整)
    var client = new TrainGrpcServiceClient(channel);

    // 添加认证和租户头
    var headers = new Metadata();
    headers.Add("Authorization", $"Bearer {_factory.HttpClient.DefaultRequestHeaders.Authorization.Parameter}");
    headers.Add("tenantId", CustomWebApplicationFactory<Program>.TenantId);

    // 调用gRPC方法
    var response = await client.GetNvrSetResponseAsync(new TrainConfig.Grpc.Models.Dtos.NvrSet.Requests.NvrSetRequest() { TrainId = 8 }, headers);
}

方案2:修复手动Kestrel端口配置

若坚持使用手动端口,需为测试环境配置HTTPS证书:

.ConfigureKestrel(k => 
{
    k.ConfigureEndpointDefaults(lo => lo.Protocols = HttpProtocols.Http1AndHttp2);
    k.ListenAnyIP(7034, listenOptions =>
    {
        listenOptions.Protocols = HttpProtocols.Http1AndHttp2;
        // 使用开发证书(仅测试环境)
        listenOptions.UseHttps();
        // 或指定自定义测试证书
        // listenOptions.UseHttps("test-cert.pfx", "test-password");
    });
})

同时在gRPC客户端中忽略证书验证:

var handler = new HttpClientHandler();
handler.ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator;
var httpClient = new HttpClient(handler)
{
    BaseAddress = new Uri("https://localhost:7034")
};
// 添加认证头
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _factory.HttpClient.DefaultRequestHeaders.Authorization.Parameter);
httpClient.DefaultRequestHeaders.Add("tenantId", CustomWebApplicationFactory<Program>.TenantId);

var channel = GrpcChannel.ForAddress(httpClient.BaseAddress, new GrpcChannelOptions { HttpClient = httpClient });
var client = new TrainGrpcServiceClient(channel);
// 发起调用...

方案3:直接调用服务实现(非网络调用)

如果测试不需要模拟网络请求,直接调用服务时需确保租户上下文正确:

[Fact]
public async Task Get_NvrSet_Response_ExpectData()
{
    // 获取多租户容器
    var multitenantService = _factory.Services.GetRequiredService<MultitenantContainer>();
    var tenantScope = multitenantService.GetTenantScope(CustomWebApplicationFactory<Program>.TenantId);
    
    // 在租户范围内解析服务
    var grpcResolver = tenantScope.Resolve<IGrpcResolverService>();
    var trainGrpcService = grpcResolver.ResolveService<ITrainGrpcService>(Guid.Parse(CustomWebApplicationFactory<Program>.TenantId));
    
    var response = await trainGrpcService.GetNvrSetResponseAsync(new TrainConfig.Grpc.Models.Dtos.NvrSet.Requests.NvrSetRequest() { TrainId = 8 });
}

内容的提问来源于stack exchange,提问作者ExtremeSwat

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.20 08:05:55