介绍
代理请求时,通常修改请求或响应的各个部分,以适应目标服务器的要求或流出其他数据,例如客户端的原始 IP 地址。 此过程通过Transforms实现。 为应用程序全局定义转换类型,然后各个路由提供参数以启用和配置这些转换。 原始请求对象不会由这些转换修改,只修改代理请求。
YARP 包括一组可供使用的内置请求和响应转换。 有关详细信息,请参阅 YARP 请求和响应转换。 如果这些转换不够,则可以添加自定义转换。
RequestTransform
所有请求转换都必须派生自抽象基类 RequestTransform。 这些可以自由修改代理 HttpRequestMessage。 避免读取或修改请求正文,因为这可能会中断代理流。 另请考虑为 TransformBuilderContext 添加参数化扩展方法,以提高可发现性和易用性。
请求转换可能会有条件地生成即时响应,例如错误条件。 这样可以防止运行任何剩余的转换并防止代理请求。 这是通过将 HttpResponse.StatusCode 设置为不等于200的值,调用 HttpResponse.StartAsync(),或者写入 HttpResponse.Body 或 BodyWriter 来指示。
              AddRequestTransform 是一种 TransformBuilderContext 扩展方法,它将请求转换定义为 Func<RequestTransformContext, ValueTask>。 这样就可以创建自定义请求转换,而无需实现 RequestTransform 派生类。
ResponseTransform
所有响应转换都必须派生自抽象基类 ResponseTransform。 这些可以自由修改客户端 HttpResponse。 避免读取或修改响应正文,因为这可能会中断代理流。 另请考虑添加参数化扩展方法 TransformBuilderContext ,以便于可发现性和易于使用。
              AddResponseTransform 是一种 TransformBuilderContext 扩展方法,能够将响应转换表述为 Func<ResponseTransformContext, ValueTask>。 这样就可以创建自定义响应转换,而无需实现 ResponseTransform 派生类。
ResponseTrailersTransform
所有响应尾部转换器都必须派生自抽象基类 ResponseTrailersTransform。 这些可以自由修改客户端 HttpResponse 尾部。 这些程序在响应正文之后执行,不应尝试修改响应标头或正文。 另请考虑添加参数化扩展方法 TransformBuilderContext ,以便于可发现性和易于使用。
              AddResponseTrailersTransform 是一种 TransformBuilderContext 扩展方法,用于将响应预告片转换定义为 . Func<ResponseTrailersTransformContext, ValueTask>. 这允许创建自定义响应尾部转换,而无需实现 ResponseTrailersTransform 派生类。
请求正文转换
YARP 不提供任何用于修改请求正文的内置转换。 但是,可以通过自定义转换来修改正文。
请注意修改了哪些类型的请求、数据缓存量、实施超时、解析不受信任的输入,以及更新与正文相关的标头,如 Content-Length。
下面的示例使用简单、低效的缓冲来转换请求。 更高效的实现将把 HttpContext.Request.Body 包装并替换为一个流,该流在数据从客户端代理到服务器时执行所需的修改。 这还需要删除 Content-Length 头,因为无法提前知道最终长度。
此示例需要 YARP 1.1,请参阅 https://github.com/microsoft/reverse-proxy/pull/1569。
.AddTransforms(context =>
{
    context.AddRequestTransform(async requestContext =>
    {
        using var reader =
            new StreamReader(requestContext.HttpContext.Request.Body);
        // TODO: size limits, timeouts
        var body = await reader.ReadToEndAsync();
        if (!string.IsNullOrEmpty(body))
        {
            body = body.Replace("Alpha", "Charlie");
            var bytes = Encoding.UTF8.GetBytes(body);
            // Change Content-Length to match the modified body, or remove it
            requestContext.HttpContext.Request.Body = new MemoryStream(bytes);
            // Request headers are copied before transforms are invoked, update any
            // needed headers on the ProxyRequest
            requestContext.ProxyRequest.Content.Headers.ContentLength =
                bytes.Length;
        }
    });
});
自定义转换只能修改请求正文(如果已存在)。 它们无法向没有正文的请求中添加新的正文(例如,没有正文的 POST 请求或 GET 请求)。 如果需要为特定的 HTTP 方法和路由添加正文,您必须在 YARP 之前运行的中间件中完成此操作,而不是在转换中进行。
以下中间件演示如何向没有正文的请求添加正文:
public class AddRequestBodyMiddleware
{
    private readonly RequestDelegate _next;
    public AddRequestBodyMiddleware(RequestDelegate next)
    {
        _next = next;
    }
    public async Task InvokeAsync(HttpContext context)
    {
        // Only modify specific route and method
        if (context.Request.Method == HttpMethods.Get &&
            context.Request.Path == "/special-route")
        {
            var bodyContent = "key=value";
            var bodyBytes = Encoding.UTF8.GetBytes(bodyContent);
            // Create a new request body
            context.Request.Body = new MemoryStream(bodyBytes);
            context.Request.ContentLength = bodyBytes.Length;
            // Replace IHttpRequestBodyDetectionFeature so YARP knows
            // a body is present
            context.Features.Set<IHttpRequestBodyDetectionFeature>(
                new CustomBodyDetectionFeature());
        }
        await _next(context);
    }
    // Helper class to indicate the request can have a body
    private class CustomBodyDetectionFeature : IHttpRequestBodyDetectionFeature
    {
        public bool CanHaveBody => true;
    }
}
注释
在中间件中,可以通过 context.GetRouteModel().Config.RouteId 为特定的 YARP 路由有条件地应用此逻辑。
响应正文转换
YARP 不提供任何用于修改响应正文的内置转换。 但是,可以通过自定义转换来修改正文。
请注意修改了哪些类型的响应、数据缓存量、实施超时、解析不受信任的输入,以及更新与正文相关的标头,如 Content-Length。 在修改内容之前,可能需要解压缩内容,如 Content-Encoding 标头所指示,然后重新压缩或删除标头。
下面的示例使用简单、低效的缓冲来转换响应。 更高效的实现会将 ReadAsStreamAsync() 返回的流与执行所需修改的流整合在一起,因为数据会从客户端代理到服务器。 这还需要删除 Content-Length 头,因为无法提前知道最终长度。
.AddTransforms(context =>
{
    context.AddResponseTransform(async responseContext =>
    {
        var stream =
            await responseContext.ProxyResponse.Content.ReadAsStreamAsync();
        using var reader = new StreamReader(stream);
        // TODO: size limits, timeouts
        var body = await reader.ReadToEndAsync();
        if (!string.IsNullOrEmpty(body))
        {
            responseContext.SuppressResponseBody = true;
            body = body.Replace("Bravo", "Charlie");
            var bytes = Encoding.UTF8.GetBytes(body);
            // Change Content-Length to match the modified body, or remove it
            responseContext.HttpContext.Response.ContentLength = bytes.Length;
            // Response headers are copied before transforms are invoked, update
            // any needed headers on the HttpContext.Response
            await responseContext.HttpContext.Response.Body.WriteAsync(bytes);
        }
    });
});
ITransformProvider
              ITransformProvider 提供上述描述的 AddTransforms 功能,以及 DI 集成和验证支持。
              通过调用 ITransformProvider 可以在 DI 中注册 AddTransforms。 可以注册多个 ITransformProvider 实现,所有实现都将运行。
              ITransformProvider 有两种方法,Validate 和 Apply。 
              Validate 让你有机会检查配置转换(如自定义元数据)所需的任何参数的路由,并返回上下文中的验证错误(如果需要的值缺失或无效)。 
              Apply 方法提供的功能与上述 AddTransform 方法提供的功能相同,并且此方法会按路由添加和配置转换。
services.AddReverseProxy()
    .LoadFromConfig(_configuration.GetSection("ReverseProxy"))
    .AddTransforms<MyTransformProvider>();
internal class MyTransformProvider : ITransformProvider
{
    public void ValidateRoute(TransformRouteValidationContext context)
    {
        // Check all routes for a custom property and validate the associated
        // transform data
        if (context.Route.Metadata?.TryGetValue("CustomMetadata", out var value) ??
            false)
        {
            if (string.IsNullOrEmpty(value))
            {
                context.Errors.Add(new ArgumentException(
                    "A non-empty CustomMetadata value is required"));
            }
        }
    }
    public void ValidateCluster(TransformClusterValidationContext context)
    {
        // Check all clusters for a custom property and validate the associated
        // transform data.
        if (context.Cluster.Metadata?.TryGetValue("CustomMetadata", out var value)
            ?? false)
        {
            if (string.IsNullOrEmpty(value))
            {
                context.Errors.Add(new ArgumentException(
                    "A non-empty CustomMetadata value is required"));
            }
        }
    }
    public void Apply(TransformBuilderContext transformBuildContext)
    {
        // Check all routes for a custom property and add the associated transform.
        if ((transformBuildContext.Route.Metadata?.TryGetValue("CustomMetadata",
            out var value) ?? false)
            || (transformBuildContext.Cluster?.Metadata?.TryGetValue(
            "CustomMetadata", out value) ?? false))
        {
            if (string.IsNullOrEmpty(value))
            {
                throw new ArgumentException(
                    "A non-empty CustomMetadata value is required");
            }
            transformBuildContext.AddRequestTransform(transformContext =>
            {
                transformContext.ProxyRequest.Options.Set(
                    new HttpRequestOptionsKey<string>("CustomMetadata"), value);
                return default;
            });
        }
    }
}
ITransformFactory
想要将其自定义转换与 Transforms 配置部分集成的开发人员可以实现一个 ITransformFactory。 这应该使用 AddTransformFactory<T>() 方法在 DI 中进行注册。 可以注册多个工厂,而且所有工厂都会被使用。
              ITransformFactory 提供两种方法,分别是 Validate 和 Build。 这些过程一次处理一组转换值,以 IReadOnlyDictionary<string, string> 表示。
加载配置时调用Validate方法以验证内容并报告所有错误。 任何报告的错误都会阻止应用配置。
该方法 Build 采用给定的配置,并为路由生成关联的转换实例。
services.AddReverseProxy()
    .LoadFromConfig(_configuration.GetSection("ReverseProxy"))
    .AddTransformFactory<MyTransformFactory>();
internal class MyTransformFactory : ITransformFactory
{
    public bool Validate(TransformRouteValidationContext context,
        IReadOnlyDictionary<string, string> transformValues)
    {
        if (transformValues.TryGetValue("CustomTransform", out var value))
        {
            if (string.IsNullOrEmpty(value))
            {
                context.Errors.Add(new ArgumentException(
                    "A non-empty CustomTransform value is required"));
            }
            return true; // Matched
        }
        return false;
    }
    public bool Build(TransformBuilderContext context,
        IReadOnlyDictionary<string, string> transformValues)
    {
        if (transformValues.TryGetValue("CustomTransform", out var value))
        {
            if (string.IsNullOrEmpty(value))
            {
                throw new ArgumentException(
                    "A non-empty CustomTransform value is required");
            }
            context.AddRequestTransform(transformContext =>
            {
                transformContext.ProxyRequest.Options.Set(
                    new HttpRequestOptionsKey<string>("CustomTransform"), value);
                return default;
            });
            return true;
        }
        return false;
    }
}
              Validate 和 Build 如果已将给定的转换配置标识为它们拥有的配置,则它们会返回 true。 A ITransformFactory 可以实现多个转换。 任何未被任何 RouteConfig.Transforms 处理的 ITransformFactory 条目都会被视为配置错误,并阻止配置生效。
考虑在RouteConfig上添加参数化扩展方法,例如WithTransformQueryValue,以便于程序化路由构建。
public static RouteConfig WithTransformQueryValue(this RouteConfig routeConfig,
    string queryKey, string value, bool append = true)
{
    var type = append ? QueryTransformFactory.AppendKey :
        QueryTransformFactory.SetKey;
    return routeConfig.WithTransform(transform =>
    {
        transform[QueryTransformFactory.QueryValueParameterKey] = queryKey;
        transform[type] = value;
    });
}