基于插件化 + Scriban 模板引擎的高效 HTTP 协议中心设计

问题

在一些集成系统中,我们通常会需要对接多个外部Http接口,较为传统的做法是:写一个 Service,定义一套 Request/Response 实体,然后封装 HttpClient。但是这种情况下,不禁要思考:接口比较多时,代码较为冗余,业务逻辑被淹没在协议拼装中。为解决此问题:分享一套插件化Http协议发送引擎。核心思路:将协议定义与发送逻辑剥离,利用 Scriban 模板引擎实现动态适配。

一、核心设计

目标:新增一个协议,只需要新增一个插件,无需修改引擎代码。设计分为三层:

1.数据抽象层:获得原始对象BusinessData

2.协议插件层:将原始数据转换为模版所需要变量池

3.执行引擎层:渲染模版并执行最终Http请求

二、核心接口定义:IHttpPlugin

cs 复制代码
/// <summary>
/// HTTP 协议插件抽象接口
/// </summary>
public interface IHttpPlugin
{
    // 插件唯一标识,用于引擎索引
    string PluginId { get; }
    
    // 协议元数据(如 URL、Method、超时时间等)
    ProtocolMetadata Metadata { get; }
    
    /// <summary>
    /// 核心转换逻辑:将业务数据映射为模板变量池
    /// </summary>
    Task<IDictionary<string, object>> MapToContextAsync(object businessData);

    /// <summary>
    /// 针对不同协议的特殊处理(如动态签名、特殊加密等)
    /// </summary>
    Task SpecialContextAsync(HttpRequestMessage request, object data, string rawJson);
}

设计思路:

MapToContextAsync:解决字段名不一致问题

SpecialContextAsync:给复杂场景留了"后门"。比如某些接口需要Aes加密等等

三、 Scriban 模板引擎

"Scriban"可能是指一种模板引擎,它主要用于.NET平台,用来生成文本输出,如HTML、电子邮件等。

为什么选择 Scriban?

性能高(Scriban 的解析速度快)

逻辑支持(支持if/else,for循环)

cs 复制代码
{
    "bCode": "{{ b_code }}",
    "eDataList": [
        {% for item in items %}
        {
            "eNo": "{{ item.no }}",
            "xPhotos": [ {{ item.photos | array.join ',' }} ]
        }
        {% endfor %}
    ]
}

四、 自动化发送引擎

自动发送引擎流程:找到插件 -> 准备上下文 -> 渲染模板 -> 发送。

cs 复制代码
public async Task<PResponse> SendThirdPartyAsync(string pluginId, object businessData)
{
    // 1. 获取对应插件
    var plugin = _provider.GetPlugin(pluginId);
    // 1. 获取平铺后的数据上下文
    var context = await plugin.MapToCpontextAsync(businessData);

    // 2. 使用 Scriban 渲染模板
    // Scriban 能够处理循环 (for)、条件 (if) 以及复杂的嵌套对象
    var template = Scriban.Template.Parse(plugin.Metadata.BodyTemplate);
    string finalJson = await template.RenderAsync(context);
    
    // 3. 构建 HTTP 请求
    var request = new HttpRequestMessage(new HttpMethod(plugin.Metadata.Method), plugin.Metadata.Url)
    {
        Content = new StringContent(finalJson, Encoding.UTF8, "application/json")
    };
    
    //plugin.Metadata.Headers = await plugin.MapToCpontextAsync(businessData);
    // 4.注入 Header
    foreach (var header in plugin.Metadata.Headers)
    {
        request.Headers.TryAddWithoutValidation(header.Key, header.Value);
    }

    //调用插件特殊化处理
    await plugin.SpecialContextAsync(request, businessData,finalJson);

    var response = new PResponse();
    // 5. 发送并返回结果
    var result = await _httpClient.SendAsync(request);
   

    //读取响应内容
    var responseJson = await result.Content.ReadAsStringAsync();

    response = JsonConvert.DeserializeObject<PResponse>(responseJson)!;
    if (response.Code != 200)
    {
        response.SetResponseCode((int)result.StatusCode, $"上传失败(状态码:{result.StatusCode})");
        return response;
    }
  
    return response;
}

五、实现一个HTTP接口插件

cs 复制代码
public class ImagesPlugin : IHttpPlugin
{
    public string PluginId => "ERP_ORDER_SYNC";
    
    public ProtocolMetadata Metadata => new ProtocolMetadata 
    {
        UrlTemplate = "http://api.erp.com/v1/save",
        Method = "POST",
        Headers = new Dictionary<string, string> { { "Auth-Token", "xadasd" } },
        // 这里的模板支持循环处理 eDataList
        BodyTemplate = @"
        {
            ""business_order_id"": ""{{ business_order_id}}"",
            ""DataList"": [
                {% for item in items %}
                {
                    ""eNo"": ""{{ item.no }}"",
                    ""sName"": ""{{ item.name }}"",
                    ""Images "": [ 
                        {% for img in item.Images %} ""{{ img }}"" {{ if !for.last }},{{ end }} {% endfor %} 
                    ]
                }{{ if !for.last }},{{ end }}
                {% endfor %}
            ]
        }"
    };

    public async Task<IDictionary<string, object>> MapToContextAsync(object data)
    {
        var order = data as OrderEntity;
        return new Dictionary<string, object>
        {
            { "business_order_id", order.Id },
            { "amount", order.TotalAmount }
            { "items", task.SubItems.Select(s => new {
                no = s.Id,
                 Images = s.CapturedImages 
            }
        };
    }

       public Task PrepareRequestAsync(HttpRequestMessage request, object data, string rawJson)
    {
        //1.处理加密 Body
    string encryptedBody = EncryptHelper.AESEncrypt(rawJson);
    request.Content = new StringContent(encryptedBody, Encoding.UTF8, "application/json");
    // 2. 处理特殊 Header
    var info = (SecurityImageCloudDataDto)data;
    ....
    ....
    request.Headers.Add("Auth", $"{authEncrypted}");
    request.Headers.Add("User-Agent", $"{info.MachineNumber}");
    return Task.CompletedTask;
    }
}

基于上述设计,我们实现了业务代码与Http协议解耦,复杂的报文转换交由 Scriban 模板,较为灵活。以此扩展多个不同http协议将变得更简单。

相关推荐
玩泥巴的1 天前
搭建一套.net下能落地的飞书考勤系统
c#·.net·二次开发·飞书
唐宋元明清21881 天前
.NET 本地Db数据库-技术方案选型
windows·c#
lindexi1 天前
dotnet DirectX 通过可等待交换链降低输入渲染延迟
c#·directx·d2d·direct2d·vortice
qq_454245032 天前
基于组件与行为的树状节点系统
数据结构·c#
bugcome_com2 天前
C# 类的基础与进阶概念详解
c#
雪人不是菜鸡2 天前
简单工厂模式
开发语言·算法·c#
铸人2 天前
大数分解的Shor算法-C#
开发语言·算法·c#
未来之窗软件服务2 天前
AI人工智能(二十四)错误示范ASR张量错误C#—东方仙盟练气期
开发语言·人工智能·c#·仙盟创梦ide·东方仙盟
yong99902 天前
基于C#实现的UPnP端口映射程序
开发语言·c#
三天不学习2 天前
Linux inotify 机制详解,解决“用户实例限制”问题
linux·运维·c#