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 }); } }
问题分析与解决方案
核心问题点
- 测试服务器HTTP2配置缺失:你注释掉了
.UseTestServer(),手动指定端口后,Kestrel缺少HTTPS证书配置,导致HTTPS端点无法正常启动,gRPC依赖HTTP2,因此无法建立连接。 - 服务调用方式错误:测试中直接从容器解析
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
相关产品推荐
相关产品推荐

