.NET 6/8中用Middleware实现gRPC请求转发与包装的问题
解决.NET gRPC代理服务中Middleware重路由端点的问题
问题背景
用C#结合.NET 6/8开发gRPC代理服务,需求是拦截原始gRPC请求、包装成新的gRPC请求后转发到目标服务。尝试通过自定义Middleware修改context.Request.Path并调用_next(context),但发现IEndpointFeature.Endpoint已被提前设置为原始服务端点,重置后内置路由中间件未重新执行,最终返回"Unimplemented"响应。
核心原因
ASP.NET Core的EndpointMiddleware会在请求管道早期执行,负责匹配当前请求对应的端点并写入IEndpointFeature。自定义Middleware修改Path后,EndpointMiddleware不会自动重新执行,导致后续流程仍使用原始端点或找不到匹配端点。
解决方案:手动触发端点重新匹配
通过注入路由匹配器和端点数据源,在Middleware中手动查找修改Path后的目标端点,更新IEndpointFeature和路由值,确保后续流程使用正确的端点。
步骤1:实现自定义Middleware
using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Routing; using System.IO; using ProtoBuf; // 需要安装protobuf-net NuGet包 public class GrpcProxyMiddleware { private readonly RequestDelegate _next; private readonly IRouteMatcher _routeMatcher; private readonly EndpointDataSource _endpointDataSource; public GrpcProxyMiddleware(RequestDelegate next, IRouteMatcher routeMatcher, EndpointDataSource endpointDataSource) { _next = next; _routeMatcher = routeMatcher; _endpointDataSource = endpointDataSource; } public async Task InvokeAsync(HttpContext context) { // 1. 拦截并处理原始gRPC请求 var originalPath = context.Request.Path; if (originalPath.StartsWithSegments("/GreeterServiceV1")) { // 缓存请求体(gRPC请求体只能读取一次) context.Request.EnableBuffering(); using var originalBodyStream = new MemoryStream(); await context.Request.Body.CopyToAsync(originalBodyStream); originalBodyStream.Position = 0; // 读取原始gRPC请求消息(示例为GreeterServiceV1的HelloRequest) var originalRequest = await Serializer.DeserializeAsync<HelloRequest>(originalBodyStream); originalBodyStream.Position = 0; // 包装成目标服务的请求消息(示例为GreeterServiceV2的HelloRequestV2) var wrappedRequest = new HelloRequestV2 { Name = originalRequest.Name, ForwardedFrom = "GreeterServiceV1" }; // 重置请求体为包装后的消息 context.Request.Body.SetLength(0); await Serializer.SerializeAsync(context.Request.Body, wrappedRequest); context.Request.Body.Position = 0; // 2. 修改请求路径为目标服务 context.Request.Path = originalPath.Replace("/GreeterServiceV1", "/GreeterServiceV2"); // 3. 重新匹配目标端点 var routeContext = new RouteContext(context); var matchedEndpoint = _routeMatcher.Match(routeContext, _endpointDataSource.Endpoints); if (matchedEndpoint != null) { // 更新EndpointFeature var endpointFeature = context.Features.Get<IEndpointFeature>() ?? new CustomEndpointFeature(); endpointFeature.Endpoint = matchedEndpoint; endpointFeature.RouteData = routeContext.RouteData; context.Features.Set<IEndpointFeature>(endpointFeature); // 更新路由值 var routeValuesFeature = context.Features.Get<IRouteValuesFeature>() ?? new RouteValuesFeature(); routeValuesFeature.RouteValues.Clear(); foreach (var kvp in routeContext.RouteData.Values) { routeValuesFeature.RouteValues.Add(kvp.Key, kvp.Value); } context.Features.Set<IRouteValuesFeature>(routeValuesFeature); } } // 4. 继续执行后续管道 await _next(context); } } // 自定义实现IEndpointFeature(因为内置EndpointFeature为internal) public class CustomEndpointFeature : IEndpointFeature { public Endpoint? Endpoint { get; set; } public RouteData? RouteData { get; set; } } // 自定义实现IRouteValuesFeature(可选,若内置实现不可访问) public class RouteValuesFeature : IRouteValuesFeature { public IDictionary<string, object?> RouteValues { get; } = new Dictionary<string, object?>(); }
步骤2:注册Middleware并配置服务
在Program.cs中确保Middleware注册在端点映射之前(保持原有顺序即可):
using service.Services; var builder = WebApplication.CreateBuilder(args); builder.Services.AddGrpc(); var app = builder.Build(); // 注册自定义代理Middleware app.UseMiddleware<GrpcProxyMiddleware>(); // 映射原始和目标gRPC服务 app.MapGrpcService<GreeterServiceV1>(); app.MapGrpcService<GreeterServiceV2>(); app.MapGet("/", () => "Communication with gRPC endpoints must be made through a gRPC client."); app.Run();
关键注意事项
- 请求体缓存:gRPC请求体只能读取一次,必须通过
EnableBuffering()开启缓存,才能重复读取和修改。 - 端点匹配逻辑:使用
IRouteMatcher和EndpointDataSource手动匹配端点,确保修改Path后能找到正确的目标服务端点。 - 特征更新:必须同时更新
IEndpointFeature和IRouteValuesFeature,否则后续流程无法正确识别新端点。
内容的提问来源于stack exchange,提问作者BearsEars
相关产品推荐
相关产品推荐

