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

如何在C#中测试gRPC拦截器?.NET Core实践遇测试难题

解决gRPC拦截器单元测试中continuation参数模拟难题的经历

我最近维护着一个同时提供REST和gRPC接口的.NET Core应用,原本用JWT中间件处理认证授权一直很顺畅。后来需要给gRPC接口加上ApiKey(GUID格式)和客户密钥的验证逻辑,于是写了个AuthorizeApplicationKeyInterceptor拦截器,用来拦截所有UnaryServerHandler调用,通过反射读取服务方法上的AuthorizeApplicationKeyAttribute来做权限校验。

功能在生产环境跑起来没问题,但一到单元测试环节就卡壳了——怎么都没法正确模拟UnaryServerMethod<TRequest, TResponse>类型的continuation参数。核心问题出在拦截器里依赖continuation.Target.GetType()获取服务类型的逻辑:

  • 实际客户端调用时,continuation.Target是ResolvedInterceptorInvoker类型,我能通过它的泛型参数拿到正确的服务类型
  • 但单元测试时,模拟出来的continuation的Target直接就是被调用的方法本身,原来的泛型参数解析逻辑完全走不通

我试了两种方案都没搞定:

  • 用Moq这类框架直接模拟continuation对象,但不管怎么配置,Target的类型都和生产环境不一致,反射逻辑总是抛出异常
  • 搭建测试用的gRPC服务器,想模拟真实调用场景,但还是没解决Target类型不匹配的问题

折腾了好一阵后,我决定换个思路——放弃拦截器方案,改用中间件来实现相同的ApiKey验证逻辑。中间件可以直接从HttpContext里获取gRPC的请求信息,而且单元测试起来要简单得多,不用再纠结continuation的Target类型问题。

附上我原来写的拦截器代码,供大家参考:

internal class AuthorizeApplicationKeyInterceptor : Interceptor
{
    /// <summary>
    /// Will find and return all AuthorizeApplicationKeyAttribute that are set on the method.
    /// If more than one attribute is set, that means that the application must have at least one role for each attribute.
    /// Else the application only needs to have one of the roles in the attribute.
    /// </summary>
    /// <param name="serviceType">The service where we will get the method from</param>
    /// <param name="methodName">The method where we will find the attributes.</param>
    /// <param name="inputParameterTypes"></param>
    /// <returns>If no attributes exist on the method; null is returned, else all attributes.</returns>
    private AuthorizeApplicationKeyAttribute[] GetAuthorizeApplicationKeyAttributeForMethod(Type serviceType, string methodName, Type[] inputParameterTypes)
    {
        MethodInfo methodInfo = serviceType.GetMethod(methodName, inputParameterTypes);
        var attributes = methodInfo?.GetCustomAttributes(typeof(AuthorizeApplicationKeyAttribute), true);
        if (attributes == null || attributes.Length == 0)
            return null;

        return attributes as AuthorizeApplicationKeyAttribute[];
    }

    public override async Task<TResponse> UnaryServerHandler<TRequest, TResponse>(
        TRequest request, ServerCallContext context, UnaryServerMethod<TRequest, TResponse> continuation)
    {
        var targetType = continuation.Target.GetType();
        // Get the service type for the service we're calling a method in.
        var serviceType = targetType.GenericTypeArguments[0];

        // Get the method name by splitting the context.Method and get the last part that should be the method name.
        var method = context.Method;
        var methodSplit = method.Split('/');
        var requestedMethodName = methodSplit.Last();

        // Get the input type for the method so we find the correct method even if we've two methods that are called the same but have different input parameters.
        Type[] types = new List<Type>() { targetType.GenericTypeArguments[1], typeof(ServerCallContext) }.ToArray();

        // Get the attributes for the method. If null is returned then no attributes were found.
        var attributes = GetAuthorizeApplicationKeyAttributeForMethod(serviceType, requestedMethodName, types);

        if (attributes != null)
        {
            var httpContext = context.GetHttpContext();
            var applicationKey = context.ApplicationKey();

            // Validate that the application key has the expected roles.
            // If we have multiple attributes then the application must have at least one of the roles for every attribute.
            foreach (AuthorizeApplicationKeyAttribute attribute in attributes)
            {
                await attribute.ValidateApplicationKey(httpContext, applicationKey);
            }
        }

        return await continuation(request, context);
    }
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.09 19:38:03