Compare commits
3 Commits
52f4b7616e
...
4839366227
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4839366227 | ||
|
|
09957364e2 | ||
|
|
a96ba1f3d2 |
40
.gitea/workflows/nuget.yml
Normal file
40
.gitea/workflows/nuget.yml
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
name: Build and Publish NuGet Package
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
paths:
|
||||||
|
- 'src/Fengling.Gateway.Plugin.Abstractions/**'
|
||||||
|
release:
|
||||||
|
types: [published]
|
||||||
|
|
||||||
|
env:
|
||||||
|
REGISTRY: gitea.shtao1.cn
|
||||||
|
PACKAGE_NAME: Fengling.Gateway.Plugin.Abstractions
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
publish:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup .NET
|
||||||
|
uses: actions/setup-dotnet@v4
|
||||||
|
with:
|
||||||
|
dotnet-version: '10.0.103'
|
||||||
|
|
||||||
|
- name: Restore dependencies
|
||||||
|
run: dotnet restore src/Fengling.Gateway.Plugin.Abstractions/Fengling.Gateway.Plugin.Abstractions.csproj
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: dotnet build src/Fengling.Gateway.Plugin.Abstractions/Fengling.Gateway.Plugin.Abstractions.csproj --configuration Release --no-restore
|
||||||
|
|
||||||
|
- name: Pack
|
||||||
|
run: dotnet pack src/Fengling.Gateway.Plugin.Abstractions/Fengling.Gateway.Plugin.Abstractions.csproj --configuration Release --no-build -o ./nupkg
|
||||||
|
|
||||||
|
- name: Publish to Gitea
|
||||||
|
run: |
|
||||||
|
dotnet nuget push ./nupkg/*.nupkg \
|
||||||
|
--source ${{ env.REGISTRY }}/api/packages/${{ github.repository_owner }}/nuget/index.json \
|
||||||
|
--username fengling \
|
||||||
|
--password ${{ secrets.GITEATOKEN }}
|
||||||
712
.planning/gateway-plugin-system.md
Normal file
712
.planning/gateway-plugin-system.md
Normal file
@ -0,0 +1,712 @@
|
|||||||
|
# 网关插件系统技术方案
|
||||||
|
|
||||||
|
## 一、概述
|
||||||
|
|
||||||
|
本文档描述 YARP 网关的插件系统规划,包括 Web UI 管理界面和动态编译加载两大核心功能。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、整体架构
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────┐
|
||||||
|
│ fengling-console │ (运维后端 - Backend)
|
||||||
|
│ web 前端 │
|
||||||
|
└─────────┬───────────┘
|
||||||
|
│ HTTP API
|
||||||
|
▼
|
||||||
|
┌─────────────────────┐
|
||||||
|
│ fengling-console │ (运维服务端)
|
||||||
|
│ │
|
||||||
|
│ - 路由管理 API │ ───▶ 数据库持久化
|
||||||
|
│ - 集群管理 API │ ───▶ Redis Pub/Sub (发布事件)
|
||||||
|
│ - 插件管理 API │
|
||||||
|
└─────────┬───────────┘
|
||||||
|
▲
|
||||||
|
│ 事件订阅
|
||||||
|
│
|
||||||
|
┌─────────┴───────────┐
|
||||||
|
│ fengling-gateway │ (YARP 网关多实例)
|
||||||
|
│ - YARP 代理 │
|
||||||
|
│ - 插件执行 │
|
||||||
|
│ - 事件监听 │
|
||||||
|
└─────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 项目职责
|
||||||
|
|
||||||
|
| 项目 | 职责 |
|
||||||
|
|------|------|
|
||||||
|
| **fengling-gateway** | 纯 YARP 代理 + 事件订阅 + 插件执行 |
|
||||||
|
| **fengling-console** | 运维 API + 配置持久化 + 事件发布 |
|
||||||
|
| **fengling-console-web** | 前端 UI (Monaco Editor) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、Web UI 管理界面
|
||||||
|
|
||||||
|
### 3.1 技术选型
|
||||||
|
|
||||||
|
| 项目 | 选择 | 理由 |
|
||||||
|
|------|------|------|
|
||||||
|
| 前端框架 | React/Vue | 独立前端项目 |
|
||||||
|
| 编辑器 | Monaco Editor | VS Code 同款,体验一致 |
|
||||||
|
| 路由 | `/gateway` | 运维平台内统一路由 |
|
||||||
|
|
||||||
|
### 3.2 功能模块
|
||||||
|
|
||||||
|
```
|
||||||
|
/gateway
|
||||||
|
├── 路由管理 (Routes)
|
||||||
|
│ ├── 列表/搜索
|
||||||
|
│ ├── 创建/编辑/删除
|
||||||
|
│ └── 路由规则配置
|
||||||
|
├── 集群管理 (Clusters)
|
||||||
|
│ ├── 上下游服务列表
|
||||||
|
│ ├── 实例管理
|
||||||
|
│ └── 健康状态
|
||||||
|
├── 插件管理 (Plugins)
|
||||||
|
│ ├── 已加载插件列表
|
||||||
|
│ ├── 上传 DLL
|
||||||
|
│ └── 在线编写 C# 代码
|
||||||
|
└── 监控统计
|
||||||
|
├── QPS/延迟
|
||||||
|
└── 流量图表
|
||||||
|
```
|
||||||
|
|
||||||
|
## 一、概述
|
||||||
|
|
||||||
|
本文档描述 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
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 插件基础接口
|
||||||
|
/// </summary>
|
||||||
|
public interface IGatewayPlugin
|
||||||
|
{
|
||||||
|
string Name { get; }
|
||||||
|
string Version { get; }
|
||||||
|
string? Description { get; }
|
||||||
|
|
||||||
|
Task OnLoadAsync();
|
||||||
|
Task OnUnloadAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 请求处理插件
|
||||||
|
/// </summary>
|
||||||
|
public interface IRequestPlugin : IGatewayPlugin
|
||||||
|
{
|
||||||
|
/// <summary>请求到达网关前</summary>
|
||||||
|
Task<HttpContext?> OnRequestAsync(HttpContext context);
|
||||||
|
|
||||||
|
/// <summary>路由决策后</summary>
|
||||||
|
Task<HttpContext?> OnRouteMatchedAsync(HttpContext context, RouteConfig route);
|
||||||
|
|
||||||
|
/// <summary>转发到后端前</summary>
|
||||||
|
Task<HttpContext?> OnForwardingAsync(HttpContext context, HttpRequestMessage request);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 响应处理插件
|
||||||
|
/// </summary>
|
||||||
|
public interface IResponsePlugin : IGatewayPlugin
|
||||||
|
{
|
||||||
|
/// <summary>后端响应后</summary>
|
||||||
|
Task OnBackendResponseAsync(HttpContext context, HttpResponseMessage response);
|
||||||
|
|
||||||
|
/// <summary>返回客户端前</summary>
|
||||||
|
Task OnResponseFinalizingAsync(HttpContext context);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 路由转换插件
|
||||||
|
/// </summary>
|
||||||
|
public interface IRouteTransformPlugin : IGatewayPlugin
|
||||||
|
{
|
||||||
|
Task<RouteConfig> TransformRouteAsync(RouteConfig original, HttpContext context);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 负载均衡插件
|
||||||
|
/// </summary>
|
||||||
|
public interface ILoadBalancePlugin : IGatewayPlugin
|
||||||
|
{
|
||||||
|
Task<Destination> SelectDestinationAsync(
|
||||||
|
IReadOnlyList<Destination> 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<AssemblyInfo> 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<PluginPackage> 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<PluginValidationResult> 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<DllValidationResult> 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<string, object> _data = new();
|
||||||
|
|
||||||
|
public T? Get<T>(string key) => _data.TryGetValue(key, out var v) ? (T)v : default;
|
||||||
|
public void Set<T>(string key, T value) => _data[key] = value!;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 使用示例
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// 插件 A: 认证
|
||||||
|
public class AuthPlugin : IRequestPlugin
|
||||||
|
{
|
||||||
|
public async Task<HttpContext?> 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<HttpContext?> 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
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/monaco-editor@0.45.0/min/vs/loader.js"></script>
|
||||||
|
|
||||||
|
<div id="editor" style="height: 500px;"></div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
require.config({ paths: { 'vs': 'https://cdn.jsdelivr.net/npm/monaco-editor@0.45.0/min/vs' }});
|
||||||
|
|
||||||
|
require(['vs/editor/editor.main'], function() {
|
||||||
|
monaco.editor.create(document.getElementById('editor'), {
|
||||||
|
value: getEditorTemplate(),
|
||||||
|
language: 'csharp',
|
||||||
|
theme: 'vs-dark',
|
||||||
|
automaticLayout: true,
|
||||||
|
minimap: { enabled: false },
|
||||||
|
fontSize: 14,
|
||||||
|
lineNumbers: 'on',
|
||||||
|
scrollBeyondLastLine: false
|
||||||
|
});
|
||||||
|
|
||||||
|
// 注册 C# 补全
|
||||||
|
monaco.languages.registerCompletionItemProvider('csharp', {
|
||||||
|
provideCompletionItems: (model, position) => {
|
||||||
|
const suggestions = [
|
||||||
|
// 常用类型
|
||||||
|
{ label: 'HttpContext', kind: monaco.languages.CompletionItemKind.Class },
|
||||||
|
{ label: 'HttpRequest', kind: monaco.languages.CompletionItemKind.Class },
|
||||||
|
{ label: 'HttpResponse', kind: monaco.languages.CompletionItemKind.Class },
|
||||||
|
|
||||||
|
// 插件接口方法
|
||||||
|
{ label: 'OnRequestAsync', kind: monaco.languages.CompletionItemKind.Method },
|
||||||
|
{ label: 'OnResponseAsync', kind: monaco.languages.CompletionItemKind.Method },
|
||||||
|
{ label: 'TransformRouteAsync', kind: monaco.languages.CompletionItemKind.Method },
|
||||||
|
|
||||||
|
// 常用属性
|
||||||
|
{ label: 'ctx.Request', kind: monaco.languages.CompletionItemKind.Property },
|
||||||
|
{ label: 'ctx.Response', kind: monaco.languages.CompletionItemKind.Property },
|
||||||
|
{ label: 'ctx.Items', kind: monaco.languages.CompletionItemKind.Property },
|
||||||
|
];
|
||||||
|
return { suggestions };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 编辑器模板
|
||||||
|
|
||||||
|
```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<HttpContext?> OnRequestAsync(HttpContext ctx)
|
||||||
|
{{
|
||||||
|
// 编写你的逻辑
|
||||||
|
{userCode}
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
";
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4.4 插件生命周期管理
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public class PluginManager
|
||||||
|
{
|
||||||
|
private readonly Dictionary<string, PluginInstance> _plugins = new();
|
||||||
|
private readonly RoslynPluginCompiler _compiler = new();
|
||||||
|
|
||||||
|
public async Task<PluginInstance> 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<MetadataReference> _defaultReferences;
|
||||||
|
|
||||||
|
public RoslynPluginCompiler()
|
||||||
|
{
|
||||||
|
_defaultReferences = GetDefaultReferences();
|
||||||
|
}
|
||||||
|
|
||||||
|
public CompileResult Compile(string sourceCode, string pluginName, IEnumerable<string> 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<MetadataReference> 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
|
||||||
|
<!-- 核心编译 -->
|
||||||
|
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.13.0" />
|
||||||
|
|
||||||
|
<!-- Scripting API (可选) -->
|
||||||
|
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Scripting" Version="4.13.0" />
|
||||||
|
|
||||||
|
<!-- 依赖解析 -->
|
||||||
|
<PackageReference Include="System.Runtime.Loader" Version="4.3.0" />
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 八、总结
|
||||||
|
|
||||||
|
本方案实现了:
|
||||||
|
|
||||||
|
1. **Web UI 管理**:类 SwaggerUI 风格的可视化界面
|
||||||
|
2. **动态编译**:Roslyn 在线编译 C# 代码
|
||||||
|
3. **插件加载**:独立 AssemblyLoadContext,支持热卸载
|
||||||
|
4. **灵活扩展**:支持简单场景(使用已有程序集)和复杂场景(上传 ZIP)
|
||||||
|
5. **流程控制**:插件可分配到 5 个不同阶段执行
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*文档版本: 1.0*
|
||||||
|
*最后更新: 2026-03-01*
|
||||||
@ -5,6 +5,9 @@
|
|||||||
</Folder>
|
</Folder>
|
||||||
<Folder Name="/src/">
|
<Folder Name="/src/">
|
||||||
<Project Path="src/YarpGateway.csproj" />
|
<Project Path="src/YarpGateway.csproj" />
|
||||||
|
<Project Path="src/Fengling.Gateway.Plugin.Abstractions/Fengling.Gateway.Plugin.Abstractions.csproj" />
|
||||||
|
</Folder>
|
||||||
|
<Project Path="src/YarpGateway.csproj" />
|
||||||
</Folder>
|
</Folder>
|
||||||
<Folder Name="/tests/">
|
<Folder Name="/tests/">
|
||||||
<Project Path="tests/YarpGateway.Tests/YarpGateway.Tests.csproj" />
|
<Project Path="tests/YarpGateway.Tests/YarpGateway.Tests.csproj" />
|
||||||
|
|||||||
@ -12,6 +12,10 @@
|
|||||||
<PackageVersion Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.2" />
|
<PackageVersion Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.2" />
|
||||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.2" />
|
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.2" />
|
||||||
<PackageVersion Include="Microsoft.EntityFrameworkCore" Version="10.0.2" />
|
<PackageVersion Include="Microsoft.EntityFrameworkCore" Version="10.0.2" />
|
||||||
|
<PackageVersion Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.2" />
|
||||||
|
<PackageVersion Include="Microsoft.AspNetCore.Http.Abstractions" Version="10.0.0" />
|
||||||
|
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.2" />
|
||||||
|
<PackageVersion Include="Microsoft.EntityFrameworkCore" Version="10.0.2" />
|
||||||
|
|
||||||
<!-- Database -->
|
<!-- Database -->
|
||||||
<PackageVersion Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.0" />
|
<PackageVersion Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.0" />
|
||||||
|
|||||||
@ -0,0 +1,22 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<RootNamespace>Fengling.Gateway.Plugin.Abstractions</RootNamespace>
|
||||||
|
|
||||||
|
<!-- NuGet Package Metadata -->
|
||||||
|
<PackageId>Fengling.Gateway.Plugin.Abstractions</PackageId>
|
||||||
|
<Version>1.0.0</Version>
|
||||||
|
<Authors>Fengling</Authors>
|
||||||
|
<Company>Fengling</Company>
|
||||||
|
<Product>Fengling Gateway Plugin</Product>
|
||||||
|
<Description>Gateway plugin abstractions for YARP reverse proxy</Description>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Yarp.ReverseProxy" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
63
src/Fengling.Gateway.Plugin.Abstractions/IGatewayPlugin.cs
Normal file
63
src/Fengling.Gateway.Plugin.Abstractions/IGatewayPlugin.cs
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Yarp.ReverseProxy.Configuration;
|
||||||
|
using Yarp.ReverseProxy.Model;
|
||||||
|
|
||||||
|
namespace Fengling.Gateway.Plugin.Abstractions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 插件基础接口
|
||||||
|
/// </summary>
|
||||||
|
public interface IGatewayPlugin
|
||||||
|
{
|
||||||
|
string Name { get; }
|
||||||
|
string Version { get; }
|
||||||
|
string? Description { get; }
|
||||||
|
|
||||||
|
Task OnLoadAsync();
|
||||||
|
Task OnUnloadAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 请求处理插件
|
||||||
|
/// </summary>
|
||||||
|
public interface IRequestPlugin : IGatewayPlugin
|
||||||
|
{
|
||||||
|
/// <summary>请求到达网关前</summary>
|
||||||
|
Task<HttpContext?> OnRequestAsync(HttpContext context);
|
||||||
|
|
||||||
|
/// <summary>路由决策后</summary>
|
||||||
|
Task<HttpContext?> OnRouteMatchedAsync(HttpContext context, RouteConfig route);
|
||||||
|
|
||||||
|
/// <summary>转发到后端前</summary>
|
||||||
|
Task<HttpContext?> OnForwardingAsync(HttpContext context, HttpRequestMessage request);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 响应处理插件
|
||||||
|
/// </summary>
|
||||||
|
public interface IResponsePlugin : IGatewayPlugin
|
||||||
|
{
|
||||||
|
/// <summary>后端响应后</summary>
|
||||||
|
Task OnBackendResponseAsync(HttpContext context, HttpResponseMessage response);
|
||||||
|
|
||||||
|
/// <summary>返回客户端前</summary>
|
||||||
|
Task OnResponseFinalizingAsync(HttpContext context);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 路由转换插件
|
||||||
|
/// </summary>
|
||||||
|
public interface IRouteTransformPlugin : IGatewayPlugin
|
||||||
|
{
|
||||||
|
Task<RouteConfig> TransformRouteAsync(RouteConfig original, HttpContext context);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 负载均衡插件
|
||||||
|
/// </summary>
|
||||||
|
public interface ILoadBalancePlugin : IGatewayPlugin
|
||||||
|
{
|
||||||
|
Task<DestinationState> SelectDestinationAsync(
|
||||||
|
IReadOnlyList<DestinationState> destinations,
|
||||||
|
HttpContext context);
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user