# 网关插件系统技术方案 ## 一、概述 本文档描述 YARP 网关的插件系统规划,包括 Web UI 管理界面和动态编译加载两大核心功能。 --- ## 二、Web UI 管理界面 ### 2.1 技术选型 | 项目 | 选择 | 理由 | |------|------|------| | 框架 | Razor Pages | 嵌入主应用,单项目部署 | | 路由 | `/gateway/ui` | 参考 SwaggerUI 风格 | | 编辑器 | Monaco Editor | VS Code 同款,体验一致 | ### 2.2 功能模块 ``` /gateway/ui ├── 路由管理 (Routes) │ ├── 列表/搜索 │ ├── 创建/编辑/删除 │ └── 路由规则配置 ├── 集群管理 (Clusters) │ ├── 上下游服务列表 │ ├── 实例管理 │ └── 健康状态 ├── 插件管理 (Plugins) │ ├── 已加载插件列表 │ ├── 上传 DLL │ └── 在线编写 C# 代码 └── 监控统计 ├── QPS/延迟 └── 流量图表 ``` --- ## 三、插件系统架构 ### 3.1 插件类型定义 ```csharp namespace Fengling.Gateway.Plugin.Abstractions { /// /// 插件基础接口 /// public interface IGatewayPlugin { string Name { get; } string Version { get; } string? Description { get; } Task OnLoadAsync(); Task OnUnloadAsync(); } /// /// 请求处理插件 /// public interface IRequestPlugin : IGatewayPlugin { /// 请求到达网关前 Task OnRequestAsync(HttpContext context); /// 路由决策后 Task OnRouteMatchedAsync(HttpContext context, RouteConfig route); /// 转发到后端前 Task OnForwardingAsync(HttpContext context, HttpRequestMessage request); } /// /// 响应处理插件 /// public interface IResponsePlugin : IGatewayPlugin { /// 后端响应后 Task OnBackendResponseAsync(HttpContext context, HttpResponseMessage response); /// 返回客户端前 Task OnResponseFinalizingAsync(HttpContext context); } /// /// 路由转换插件 /// public interface IRouteTransformPlugin : IGatewayPlugin { Task TransformRouteAsync(RouteConfig original, HttpContext context); } /// /// 负载均衡插件 /// public interface ILoadBalancePlugin : IGatewayPlugin { Task SelectDestinationAsync( IReadOnlyList destinations, HttpContext context); } } ``` ### 3.2 插件阶段枚举 ```csharp public enum PipelineStage { None = 0, OnRequest = 1, // 请求到达网关前 OnRoute = 2, // 路由决策时 OnRequestBackend = 3, // 转发到后端前 OnResponseBackend = 4, // 后端响应后 OnResponse = 5 // 返回给客户端前 } ``` --- ## 四、核心模块设计 ### 4.1 依赖管理(A方案) #### 简单场景:直接使用网关已有程序集 ```csharp // API 暴露网关程序集 [ApiController] public class AssembliesController : ControllerBase { [HttpGet("available")] public List GetAvailableAssemblies() { return AppDomain.CurrentDomain.GetAssemblies() .Where(a => !a.IsDynamic && !string.IsNullOrEmpty(a.Location)) .Select(a => new AssemblyInfo { Name = a.GetName().Name, Version = a.GetName().Version?.ToString(), Location = a.Location }) .OrderBy(a => a.Name) .ToList(); } } ``` #### 复杂场景:上传 ZIP 包 ```csharp public class PluginUploadService { private readonly IObjectStorage _storage; private readonly PluginDbContext _db; private readonly string _localPluginPath = "/app/plugins"; public async Task UploadPluginAsync(IFormFile zipFile, string pluginName) { // 1. 上传到对象存储 var storageKey = $"plugins/{Guid.NewGuid()}/{zipFile.FileName}"; await _storage.UploadAsync(zipFile.OpenReadStream(), storageKey); // 2. 保存到数据库 var plugin = new PluginPackage { Id = Guid.NewGuid(), Name = pluginName, StorageKey = storageKey, UploadedAt = DateTime.UtcNow, Status = PluginStatus.Pending }; await _db.PluginPackages.AddAsync(plugin); await _db.SaveChangesAsync(); return plugin; } public async Task ExtractAndLoadAsync(Guid pluginId) { var plugin = await _db.PluginPackages.FindAsync(pluginId); // 3. 下载到本地 var localDir = Path.Combine(_localPluginPath, plugin.Id.ToString()); Directory.CreateDirectory(localDir); await _storage.DownloadAsync(plugin.StorageKey, localDir + ".zip"); // 4. 解压 ZipFile.ExtractToDirectory(localDir + ".zip", localDir, overwriteFiles: true); File.Delete(localDir + ".zip"); // 5. 加载插件 await _pluginManager.LoadFromDirectoryAsync(localDir); } } ``` #### ZIP 上传验证 ```csharp public class PluginValidationService { public async Task ValidateAsync(Stream zipStream) { var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); await ExtractZipAsync(zipStream, tempDir); try { var dlls = Directory.GetFiles(tempDir, "*.dll"); var result = new PluginValidationResult(); foreach (var dll in dlls) { var dllResult = await ValidateDllAsync(dll); result.Assemblies.Add(dllResult); } var validPlugins = result.Assemblies .Where(a => a.IsValidPlugin) .ToList(); if (validPlugins.Count == 0) { result.IsValid = false; result.ErrorMessage = "未找到实现 IGatewayPlugin 接口的类"; } else { result.IsValid = true; result.ValidPluginTypes = validPlugins .SelectMany(a => a.PluginTypes) .ToList(); } return result; } finally { Directory.Delete(tempDir, true); } } private async Task ValidateDllAsync(string dllPath) { var result = new DllValidationResult { DllName = Path.GetFileName(dllPath) }; try { var assembly = AssemblyLoadContext.Default.LoadFromAssemblyPath(dllPath); var pluginTypes = assembly.GetTypes() .Where(t => typeof(IGatewayPlugin).IsAssignableFrom(t)) .Where(t => !t.IsAbstract && !t.IsInterface) .ToList(); if (pluginTypes.Count > 0) { result.IsValidPlugin = true; result.PluginTypes = pluginTypes.Select(t => new PluginTypeInfo { TypeName = t.FullName, ImplementedInterfaces = t.GetInterfaces().Select(i => i.Name).ToList() }).ToList(); } } catch (Exception ex) { result.IsValid = false; result.ErrorMessage = ex.Message; } return result; } } ``` --- ### 4.2 插件间通信(B方案) 采用**方案 3:强类型 + 弱类型混合** #### 插件上下文 ```csharp public class GatewayContext { // 预定义常用字段(强类型) public string? UserId { get; set; } public string? TenantId { get; set; } public UserTier Tier { get; set; } public bool IsAuthenticated { get; set; } public DateTime RequestTime { get; set; } // 扩展数据(弱类型) public PluginDataBag Data { get; } = new(); } public class PluginDataBag { private readonly Dictionary _data = new(); public T? Get(string key) => _data.TryGetValue(key, out var v) ? (T)v : default; public void Set(string key, T value) => _data[key] = value!; } ``` #### 使用示例 ```csharp // 插件 A: 认证 public class AuthPlugin : IRequestPlugin { public async Task OnRequestAsync(HttpContext context) { var userId = ValidateToken(context); if (userId != null) { context.Items["CurrentUserId"] = userId; context.Items["IsAuthenticated"] = true; } return context; } } // 插件 B: 审计 public class AuditPlugin : IRequestPlugin { public async Task OnRequestAsync(HttpContext context) { if (context.Items.TryGetValue("CurrentUserId", out var userId)) { await _logger.LogAsync($"User {userId} accessed {context.Request.Path}"); } return context; } } ``` --- ### 4.3 在线代码编辑器(C方案) 采用 **Monaco Editor + 前端模拟补全** > **补充说明**:如需更完整的 C# IntelliSense(如真实代码分析、跳转到定义),可使用 Microsoft 官方的 **roslyn-language-server**(VS Code C# 扩展背后使用的语言服务器)。 > > **部署方式**: > ```bash > # 安装 > dotnet tool install -g Microsoft.CodeAnalysis.LanguageServer > > # 启动服务 > roslyn-languageserver --port 5000 > ``` > > 前端通过 WebSocket 连接该服务获取完整的语言特性支持。但目前阶段前端模拟补全已足够使用。 ```html
``` #### 编辑器模板 ```csharp // 生成的代码模板 $@" using System; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Fengling.Gateway.Plugin.Abstractions; public class {pluginName} : IRequestPlugin {{ public string Name => ""{pluginName}""; public string Version => ""1.0.0""; public async Task OnRequestAsync(HttpContext ctx) {{ // 编写你的逻辑 {userCode} }} }} "; ``` --- ### 4.4 插件生命周期管理 ```csharp public class PluginManager { private readonly Dictionary _plugins = new(); private readonly RoslynPluginCompiler _compiler = new(); public async Task LoadPluginAsync(byte[] assemblyBytes, string pluginName) { // 1. 隔离加载程序集 var context = new PluginLoadContext(pluginName); var assembly = context.LoadFromStream(new MemoryStream(assemblyBytes)); // 2. 查找插件入口类型 var pluginType = assembly.GetTypes() .FirstOrDefault(t => typeof(IGatewayPlugin).IsAssignableFrom(t)); if (pluginType == null) { context.Unload(); throw new InvalidOperationException("No IGatewayPlugin implementation found"); } // 3. 创建实例 var plugin = (IGatewayPlugin)Activator.CreateInstance(pluginType)!; await plugin.OnLoadAsync(); // 4. 保存实例 var instance = new PluginInstance { Name = pluginName, Assembly = assembly, Context = context, Plugin = plugin, LoadedAt = DateTime.UtcNow }; _plugins[pluginName] = instance; return instance; } public async Task UnloadPluginAsync(string pluginName) { if (!_plugins.TryGetValue(pluginName, out var instance)) return; await instance.Plugin.OnUnloadAsync(); instance.Context.Unload(); _plugins.Remove(pluginName); } } public class PluginLoadContext : AssemblyLoadContext { public PluginLoadContext(string name) : base(name, isCollectible: true) { } protected override Assembly? Load(AssemblyName assemblyName) { return null; } } ``` --- ### 4.5 插件编译服务 ```csharp public class RoslynPluginCompiler { private readonly IEnumerable _defaultReferences; public RoslynPluginCompiler() { _defaultReferences = GetDefaultReferences(); } public CompileResult Compile(string sourceCode, string pluginName, IEnumerable extraAssemblies) { var syntaxTree = CSharpSyntaxTree.ParseText(sourceCode); var references = _defaultReferences.Concat( extraAssemblies.Select(a => MetadataReference.CreateFromFile(a)) ); var compilation = CSharpCompilation.Create( assemblyName: $"Plugin_{pluginName}_{Guid.NewGuid():N}", syntaxTrees: new[] { syntaxTree }, references: references, options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary) .WithAllowUnsafe(false) .WithOptimizationLevel(OptimizationLevel.Release)); using var ms = new MemoryStream(); var emitResult = compilation.Emit(ms); if (!emitResult.Success) { return CompileResult.Fail(emitResult.Diagnostics); } ms.Seek(0, SeekOrigin.Begin); return CompileResult.Success(ms.ToArray()); } private IEnumerable GetDefaultReferences() { return AppDomain.CurrentDomain.GetAssemblies() .Where(a => !a.IsDynamic && !string.IsNullOrEmpty(a.Location)) .Select(a => MetadataReference.CreateFromFile(a.Location)); } } ``` --- ## 五、数据库模型 ```csharp public class PluginPackage { public Guid Id { get; set; } public string Name { get; set; } = string.Empty; public string Version { get; set; } = "1.0.0"; public string? Description { get; set; } public string StorageKey { get; set; } = string.Empty; public PluginStatus Status { get; set; } public DateTime UploadedAt { get; set; } public DateTime? LoadedAt { get; set; } } public class PluginPipeline { public Guid Id { get; set; } public Guid PluginId { get; set; } public PipelineStage Stage { get; set; } public int Order { get; set; } public bool IsEnabled { get; set; } } public enum PluginStatus { Pending = 0, Validated = 1, Loaded = 2, Failed = 3, Disabled = 4 } ``` --- ## 六、实施计划 | 阶段 | 任务 | 优先级 | |------|------|--------| | Phase 1 | 集成 YARP 到现有项目 | 高 | | Phase 2 | 插件基础接口定义 | 高 | | Phase 3 | 插件编译 + 加载框架 | 高 | | Phase 4 | 嵌入式 Razor UI | 中 | | Phase 5 | Monaco Editor 集成 | 中 | | Phase 6 | ZIP 上传验证功能 | 中 | | Phase 7 | 测试与优化 | 低 | --- ## 七、NuGet 依赖 ```xml ``` --- ## 八、总结 本方案实现了: 1. **Web UI 管理**:类 SwaggerUI 风格的可视化界面 2. **动态编译**:Roslyn 在线编译 C# 代码 3. **插件加载**:独立 AssemblyLoadContext,支持热卸载 4. **灵活扩展**:支持简单场景(使用已有程序集)和复杂场景(上传 ZIP) 5. **流程控制**:插件可分配到 5 个不同阶段执行 --- *文档版本: 1.0* *最后更新: 2026-03-01*