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

.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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.07 22:55:38