基于责任链与策略模式在Azure Functions中构建动态插件化网关


在处理多个微服务或后端API时,一个统一的入口点——API网关——是必要的。它负责处理认证、日志、限流、请求转换等横切关注点。然而,构建这样一个网关通常面临两个极端选择:要么采用功能全面但配置复杂且成本高昂的商业产品(如Azure API Management),要么从零开始构建一个单体的、难以维护的代理服务。当业务需求快速变化,需要频繁增删或修改网关逻辑时,这两种方案都显得僵化。

我们需要一种更灵活的架构:一个轻量级的、可插拔的网关。它的核心应该是一个稳定的管道,而具体的业务逻辑(如认证方式、路由规则)则作为“插件”动态地组合起来。这种架构不仅易于扩展,还能让不同团队独立开发和部署自己的网关插件,而无需触动核心框架。

方案权衡:走向插件化的必然性

在项目初期,我们曾评估过几种实现方式。

方案A:单一的巨无霸Azure Function

这是最直接的思路。创建一个HttpTrigger Function,内部用大量的if-elseswitch语句来处理不同的路由和逻辑。

  • 优势: 开发启动快,初期逻辑简单时非常直观。
  • 劣势: 随着逻辑增多,这个Function会迅速膨胀成一个难以维护的怪兽。违反了单一职责原则,任何微小的改动(比如调整一个API的日志级别)都需要重新部署整个Function,测试回归的范围巨大,风险极高。这种设计也使得并行开发变得异常困难。

方案B:功能完备的Azure API Management (APIM)

APIM是Azure提供的托管API网关服务,功能强大,通过策略(Policy)XML来定义处理逻辑。

  • 优势: 功能全面,从缓存、认证到限流一应俱全,稳定性有保障。
  • 劣势: 对于某些场景来说,它过于“重”。策略是用XML定义的,复杂的逻辑表达起来非常繁琐,而且缺乏编程语言的灵活性。成本是另一个重要考量,对于中小型项目或内部工具,APIM的成本可能过高。最关键的是,它的定制化能力有限,当我们需要实现非常规的认证协议或复杂的动态路由逻辑时,会感到束缚。

最终选择:基于设计模式的插件化Azure Function网关

我们决定走一条中间路线:利用Azure Functions的Serverless特性,结合经典的设计模式,构建一个我们自己的、轻量级且高度可扩展的网关。这个网关本身是一个HttpTrigger Function,但它内部不包含任何硬编码的业务逻辑。它的唯一职责是加载并执行一个由多个“插件”组成的管道。

这种架构的核心是责任链模式 (Chain of Responsibility Pattern) 和 **策略模式 (Strategy Pattern)**。

  • 责任链模式 用于构建处理管道。每个插件都是链条上的一个节点,请求会依次通过这些节点,每个节点可以选择处理请求、修改请求、或者直接中断链条并返回响应。
  • 策略模式 用于处理动态决策,尤其是在路由阶段。网关可以根据请求头、路径或其他参数,动态选择一个路由策略来决定将请求转发到哪个后端服务。

这种设计使得每个功能(认证、日志、限流、路由)都是一个独立的、可单独测试和部署的类库。网关核心保持稳定,业务逻辑通过插件的组合来动态实现。

核心实现:构建插件化处理管道

我们的目标是创建一个请求处理管道,每个插件都是管道的一部分。

1. 定义插件契约

首先,我们需要一个所有插件都必须遵守的接口。这个接口定义了插件的核心行为。

// IPluginContext.cs
using Microsoft.AspNetCore.Http;
using System.Collections.Generic;

namespace PluggableGateway.Interfaces
{
    /// <summary>
    /// 插件执行上下文,在整个请求管道中传递。
    /// 它携带了原始请求、响应构建器以及一个属性包用于插件间通信。
    /// </summary>
    public interface IPluginContext
    {
        HttpRequest HttpRequest { get; }
        HttpResponse HttpResponse { get; }
        
        /// <summary>
        /// 一个键值对集合,用于在插件之间传递自定义数据。
        /// 例如,认证插件可以将用户信息放入其中,供后续插件使用。
        /// </summary>
        IDictionary<string, object> Properties { get; }

        /// <summary>
        /// 目标后端服务的URI,由路由插件设置。
        /// </summary>
        Uri TargetUri { get; set; }
        
        /// <summary>
        /// 标记请求是否应被提前终止。
        /// 如果一个插件将此设置为true,管道将停止执行并立即返回。
        /// </summary>
        bool IsTerminated { get; set; }
    }
}

// IPlugin.cs
using System.Threading.Tasks;

namespace PluggableGateway.Interfaces
{
    /// <summary>
    /// 所有网关插件必须实现的接口。
    /// </summary>
    public interface IPlugin
    {
        /// <summary>
        /// 插件的执行顺序。值越小,越先执行。
        /// </summary>
        int Order { get; }

        /// <summary>
        /// 插件的唯一名称。
        /// </summary>
        string Name { get; }

        /// <summary>
        /// 执行插件的核心逻辑。
        /// </summary>
        /// <param name="context">请求上下文。</param>
        /// <param name="next">指向管道中下一个插件的委托。</param>
        Task ExecuteAsync(IPluginContext context, Func<IPluginContext, Task> next);
    }
}

IPluginContext 是贯穿整个处理管道的状态载体。IPlugin 接口定义了插件的基本结构,其中 ExecuteAsync 方法的设计是责任链模式的关键。它接收下一个插件的委托 next,插件可以在执行完自己的逻辑后,通过调用 await next(context) 将控制权交给下一个插件。

2. 实现管道构建器

我们需要一个机制来发现所有已注册的插件,并根据它们的Order属性将它们组装成一个可执行的管道。

// PipelineBuilder.cs
using PluggableGateway.Interfaces;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace PluggableGateway.Core
{
    public class PipelineBuilder
    {
        private readonly IEnumerable<IPlugin> _plugins;

        // 通过依赖注入获取所有实现了IPlugin接口的服务
        public PipelineBuilder(IEnumerable<IPlugin> plugins)
        {
            // 按Order属性升序排序,确保执行顺序
            _plugins = plugins.OrderBy(p => p.Order).ToList();
        }

        public Func<IPluginContext, Task> Build()
        {
            // 管道的末端是一个什么都不做的委托
            Func<IPluginContext, Task> pipeline = context => Task.CompletedTask;

            // 从后往前构建委托链。这是责任链模式的一种经典实现方式。
            // 最后一个插件的 next 指向 Task.CompletedTask。
            // 倒数第二个插件的 next 指向最后一个插件的执行委托。
            // 以此类推,直到第一个插件。
            foreach (var plugin in _plugins.Reverse())
            {
                var nextPluginInChain = pipeline;
                pipeline = context => plugin.ExecuteAsync(context, nextPluginInChain);
            }

            return pipeline;
        }
    }
}

PipelineBuilderBuild 方法是整个设计的核心。它通过一个巧妙的foreach循环,将所有插件的ExecuteAsync方法链接成一个巨大的委托链。调用最终返回的Func<IPluginContext, Task>时,请求就会按照预设的顺序流经所有插件。

3. 编写具体插件

现在我们可以开始编写具体的插件了。

认证插件 (Authentication Plugin)

这个插件检查请求头中是否包含有效的API Key。

// AuthenticationPlugin.cs
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using System.Threading.Tasks;

namespace PluggableGateway.Plugins
{
    public class AuthenticationPlugin : IPlugin
    {
        private readonly ILogger<AuthenticationPlugin> _logger;
        private readonly string _expectedApiKey;
        
        public int Order => 100;
        public string Name => "AuthenticationPlugin";

        public AuthenticationPlugin(ILogger<AuthenticationPlugin> logger, IConfiguration configuration)
        {
            _logger = logger;
            // 从配置中读取API Key,而不是硬编码
            _expectedApiKey = configuration["ExpectedApiKey"]; 
            if (string.IsNullOrEmpty(_expectedApiKey))
            {
                throw new InvalidOperationException("ExpectedApiKey is not configured.");
            }
        }

        public async Task ExecuteAsync(IPluginContext context, Func<IPluginContext, Task> next)
        {
            _logger.LogInformation("Executing {PluginName}...", Name);

            if (!context.HttpRequest.Headers.TryGetValue("X-Api-Key", out var apiKey) || apiKey != _expectedApiKey)
            {
                _logger.LogWarning("Authentication failed. Invalid or missing API Key.");
                context.HttpResponse.StatusCode = StatusCodes.Status401Unauthorized;
                // 将终止标记设为true,管道将在此处停止
                context.IsTerminated = true;
                await context.HttpResponse.WriteAsync("Unauthorized");
                return;
            }

            _logger.LogInformation("Authentication successful.");
            // 认证通过,将控制权交给下一个插件
            await next(context);
        }
    }
}

这个插件演示了如何中断管道。如果认证失败,它会设置IsTerminatedtrue并直接返回,next(context)不会被调用。

动态路由插件 (Dynamic Routing Plugin)

这是策略模式的应用场景。我们根据请求的某个特性(例如路径)来选择不同的路由策略。

首先,定义策略接口和具体策略。

// IRoutingStrategy.cs
namespace PluggableGateway.Interfaces
{
    public interface IRoutingStrategy
    {
        string Name { get; }
        Uri GetTargetUri(IPluginContext context);
    }
}

// ServiceARoutingStrategy.cs
using Microsoft.Extensions.Configuration;

namespace PluggableGateway.Routing
{
    public class ServiceARoutingStrategy : IRoutingStrategy
    {
        private readonly Uri _backendUri;
        public string Name => "ServiceA";

        public ServiceARoutingStrategy(IConfiguration configuration)
        {
            // 路由目标也从配置中读取
            _backendUri = new Uri(configuration["BackendServices:ServiceA"]);
        }

        public Uri GetTargetUri(IPluginContext context)
        {
            // 简单的直接路由
            return _backendUri;
        }
    }
}

// ServiceBRoutingStrategy.cs
namespace PluggableGateway.Routing
{
    public class ServiceBRoutingStrategy : IRoutingStrategy
    {
        private readonly Uri _backendUri;
        public string Name => "ServiceB";

        public ServiceBRoutingStrategy(IConfiguration configuration)
        {
            _backendUri = new Uri(configuration["BackendServices:ServiceB"]);
        }

        public Uri GetTargetUri(IPluginContext context)
        {
            // 可以在这里实现更复杂的逻辑,例如从路径中提取参数
            var path = context.HttpRequest.Path.Value;
            return new Uri(_backendUri, path);
        }
    }
}

然后,路由插件本身负责选择并执行策略。

// DynamicRoutingPlugin.cs
using System.Collections.Generic;
using System.Linq;

namespace PluggableGateway.Plugins
{
    public class DynamicRoutingPlugin : IPlugin
    {
        private readonly ILogger<DynamicRoutingPlugin> _logger;
        private readonly Dictionary<string, IRoutingStrategy> _strategies;

        public int Order => 200; // 在认证之后执行
        public string Name => "DynamicRoutingPlugin";

        public DynamicRoutingPlugin(ILogger<DynamicRoutingPlugin> logger, IEnumerable<IRoutingStrategy> strategies)
        {
            _logger = logger;
            // 通过依赖注入获取所有路由策略,并存入字典以便快速查找
            _strategies = strategies.ToDictionary(s => s.Name, s => s, StringComparer.OrdinalIgnoreCase);
        }

        public async Task ExecuteAsync(IPluginContext context, Func<IPluginContext, Task> next)
        {
            _logger.LogInformation("Executing {PluginName}...", Name);

            // 示例:根据请求头 'X-Route-To' 选择策略
            if (!context.HttpRequest.Headers.TryGetValue("X-Route-To", out var routeTo) ||
                !_strategies.TryGetValue(routeTo, out var strategy))
            {
                _logger.LogWarning("Routing failed. No matching strategy for header value: {RouteTo}", routeTo);
                context.HttpResponse.StatusCode = StatusCodes.Status400BadRequest;
                context.IsTerminated = true;
                await context.HttpResponse.WriteAsync("Invalid route specified.");
                return;
            }

            var targetUri = strategy.GetTargetUri(context);
            if (targetUri == null)
            {
                _logger.LogError("Strategy {StrategyName} returned a null URI.", strategy.Name);
                context.HttpResponse.StatusCode = StatusCodes.Status500InternalServerError;
                context.IsTerminated = true;
                await context.HttpResponse.WriteAsync("Routing logic failed.");
                return;
            }
            
            // 将计算出的目标URI存入上下文,供后续插件(如代理插件)使用
            context.TargetUri = targetUri;
            _logger.LogInformation("Request routed to {TargetUri} by strategy {StrategyName}", targetUri, strategy.Name);

            await next(context);
        }
    }
}

代理插件 (Proxy Plugin)

这是管道的最后一环,它负责将请求真正地转发到后端服务。

// ProxyPlugin.cs
using System.Net.Http;
using Microsoft.AspNetCore.Http.Extensions;

namespace PluggableGateway.Plugins
{
    public class ProxyPlugin : IPlugin
    {
        private readonly ILogger<ProxyPlugin> _logger;
        private readonly IHttpClientFactory _httpClientFactory;

        public int Order => 1000; // 通常是最后一个业务插件
        public string Name => "ProxyPlugin";

        public ProxyPlugin(ILogger<ProxyPlugin> logger, IHttpClientFactory httpClientFactory)
        {
            _logger = logger;
            _httpClientFactory = httpClientFactory;
        }

        public async Task ExecuteAsync(IPluginContext context, Func<IPluginContext, Task> next)
        {
            if (context.TargetUri == null)
            {
                _logger.LogError("ProxyPlugin cannot execute because TargetUri is not set in the context.");
                context.HttpResponse.StatusCode = StatusCodes.Status500InternalServerError;
                context.IsTerminated = true; // 终止管道
                await context.HttpResponse.WriteAsync("Gateway configuration error: Target URI not found.");
                return;
            }
            
            _logger.LogInformation("Proxying request to {TargetUri}", context.TargetUri);
            var client = _httpClientFactory.CreateClient("GatewayProxyClient");

            // 1. 创建转发请求
            var requestMessage = new HttpRequestMessage();
            var requestMethod = context.HttpRequest.Method;
            
            // 复制请求方法和内容
            requestMessage.Method = new HttpMethod(requestMethod);
            if (HttpMethods.IsPost(requestMethod) || HttpMethods.IsPut(requestMethod) || HttpMethods.IsPatch(requestMethod))
            {
                requestMessage.Content = new StreamContent(context.HttpRequest.Body);
            }

            // 2. 复制请求头
            foreach (var header in context.HttpRequest.Headers)
            {
                // HttpRequestMessage.Content.Headers 和 HttpRequestMessage.Headers 不能重复添加
                if (!requestMessage.Headers.TryAddWithoutValidation(header.Key, header.Value.ToArray()) && requestMessage.Content != null)
                {
                    requestMessage.Content.Headers.TryAddWithoutValidation(header.Key, header.Value.ToArray());
                }
            }
            
            // 3. 设置请求URI
            requestMessage.RequestUri = new Uri(UriHelper.BuildAbsolute(context.TargetUri.Scheme, new HostString(context.TargetUri.Host, context.TargetUri.Port), context.TargetUri.AbsolutePath, context.HttpRequest.Path, context.HttpRequest.QueryString));
            
            // 4. 发送请求
            using var responseMessage = await client.SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead, context.HttpRequest.HttpContext.RequestAborted);
            
            // 5. 将后端响应复制回客户端
            context.HttpResponse.StatusCode = (int)responseMessage.StatusCode;
            foreach (var header in responseMessage.Headers)
            {
                context.HttpResponse.Headers[header.Key] = header.Value.ToArray();
            }
            foreach (var header in responseMessage.Content.Headers)
            {
                context.HttpResponse.Headers[header.Key] = header.Value.ToArray();
            }
            
            await responseMessage.Content.CopyToAsync(context.HttpResponse.Body);
            
            // 在代理之后,通常不应再有其他插件,但为了保持管道的完整性,仍然调用 next
            await next(context);
        }
    }
}

4. 组装网关Function和依赖注入

最后,我们需要一个Azure Function作为入口点,并配置好依赖注入来注册所有插件和策略。

graph TD
    subgraph Azure Function App
        A[HttpTrigger: GatewayFunction] --> B{PipelineBuilder};
        B --> C{Plugin & Strategy DI Container};
        C --> D1[AuthPlugin];
        C --> D2[RoutingPlugin];
        C --> D3[ProxyPlugin];
        C --> S1[ServiceARoutingStrategy];
        C --> S2[ServiceBRoutingStrategy];

        A -- Builds & Executes --> P[Request Pipeline];
        P -- Request Flow --> D1;
        D1 --> D2;
        D2 -- Uses --> S1;
        D2 -- or --> S2;
        D2 --> D3;
        D3 -- Forwards to --> E[Backend Service];
    end

Startup.cs / Program.cs (for .NET Isolated worker)

// Program.cs (using .NET 7 Isolated Worker model)
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using PluggableGateway.Core;
using PluggableGateway.Interfaces;
using PluggableGateway.Plugins;
using PluggableGateway.Routing;

var host = new HostBuilder()
    .ConfigureFunctionsWorkerDefaults()
    .ConfigureServices(services =>
    {
        // 注册核心服务
        services.AddSingleton<PipelineBuilder>();
        services.AddHttpClient("GatewayProxyClient");

        // 注册所有插件
        // DI容器会自动找到所有实现了IPlugin的类
        services.AddSingleton<IPlugin, AuthenticationPlugin>();
        services.AddSingleton<IPlugin, DynamicRoutingPlugin>();
        services.AddSingleton<IPlugin, ProxyPlugin>();
        
        // 注册所有路由策略
        services.AddSingleton<IRoutingStrategy, ServiceARoutingStrategy>();
        services.AddSingleton<IRoutingStrategy, ServiceBRoutingStrategy>();
        
    })
    .Build();

host.Run();

GatewayFunction.cs

// GatewayFunction.cs
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Http;
using System.Threading.Tasks;
using PluggableGateway.Core;

namespace PluggableGateway
{
    public class GatewayFunction
    {
        private readonly PipelineBuilder _pipelineBuilder;
        private readonly ILogger<GatewayFunction> _logger;

        public GatewayFunction(PipelineBuilder pipelineBuilder, ILogger<GatewayFunction> logger)
        {
            _pipelineBuilder = pipelineBuilder;
            _logger = logger;
        }

        [Function("Gateway")]
        public async Task<HttpResponseData> Run(
            [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", "put", "delete", "patch", Route = "{*path}")] HttpRequestData req)
        {
            _logger.LogInformation("Gateway received a request.");

            var pipeline = _pipelineBuilder.Build();
            
            // 注意:在.NET Isolated模型中,HttpContext的直接访问方式有变化
            // 为了简化示例,我们假设一个上下文实现可以被构建
            // 在真实项目中,你需要一个适配器来桥接HttpRequestData和IPluginContext
            var context = new PluginContext(req); // 这是一个假设的实现

            try
            {
                await pipeline(context);
                
                // 如果管道没有生成响应,则返回一个默认的成功响应
                if (context.HttpResponse.StatusCode == 0) // 假设StatusCode为0表示未设置
                {
                    var defaultResponse = req.CreateResponse(HttpStatusCode.OK);
                    await defaultResponse.WriteStringAsync("Request processed successfully by gateway.");
                    return defaultResponse;
                }
                
                // 将IPluginContext中的响应转换回HttpResponseData
                return await context.ConvertToHttpResponseDataAsync(req);
                
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "An unhandled exception occurred in the gateway pipeline.");
                var errorResponse = req.CreateResponse(HttpStatusCode.InternalServerError);
                await errorResponse.WriteStringAsync("An unexpected error occurred.");
                return errorResponse;
            }
        }
    }
}

注意: 上述GatewayFunction.cs代码中的PluginContextConvertToHttpResponseDataAsync是伪代码,用于说明概念。在真实的.NET Isolated Worker项目中,需要编写具体的适配器逻辑来处理HttpRequestDataHttpResponseData

local.settings.json

{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "UseDevelopmentStorage=true",
    "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated",
    "ExpectedApiKey": "my-secret-api-key",
    "BackendServices:ServiceA": "https://api.service-a.com",
    "BackendServices:ServiceB": "http://localhost:7072/api/"
  }
}

架构的扩展性与局限性

这种基于插件的架构提供了极大的灵活性。需要添加一个新的功能,比如请求限流?只需编写一个RateLimitingPlugin,实现IPlugin接口,并在Startup.cs中注册它即可,无需修改任何现有代码。这完全符合开闭原则。不同团队可以并行开发自己的插件,并通过配置来决定在不同环境中启用哪些插件。

然而,这个方案也并非没有缺点。

  1. 冷启动延迟: 作为一个Azure Function,它会面临冷启动问题。对于一个需要低延迟的网关来说,这可能是一个关键的性能瓶颈。在生产环境中,必须使用Premium Plan或App Service Plan并启用”Always On”来缓解这个问题,但这会增加成本。
  2. 性能开销: 责任链中的每一次await next(context)都涉及一次异步状态机的切换。虽然开销很小,但在一个包含大量插件的高性能链条中,累积的开销可能会变得显著。它永远不会比在一个单一进程内直接调用方法快。
  3. 复杂性管理: 随着插件数量的增加,插件之间的依赖关系和执行顺序可能会变得复杂。必须有严格的文档和规范来管理Order属性,避免出现顺序错乱导致的逻辑错误。例如,路由插件必须在代理插件之前执行。
  4. 无状态限制: Azure Functions本质上是无状态的。如果插件需要共享状态(例如,用于限流的计数器),则必须依赖外部存储,如Redis或Cosmos DB,这会增加架构的复杂性和延迟。

总的来说,这种方法最适合那些需要高度定制化和快速迭代,同时又能接受Serverless所带来的延迟特性的场景。它在灵活性、可维护性和开发效率之间找到了一个很好的平衡点,是标准APIM和单体Function之间一个有价值的折中方案。


  目录