一款簡單好用的 Asp.net core 插件化開發框架

百劍閣 發佈 2022-04-24T15:52:01.101185+00:00

簡介插件化開發是一種非常常用的開發模式,通常是一個非常小的核心模塊加各種各樣的功能插件。如果你使用過一些開源 CMS 的話,肯定會用過其中的的插件功能,用戶可以通過啟用或者上傳插件包的方式動態添加一些功能,那麼在 ASP.NET Core 中如何實現插件化開發呢。

簡介

插件化開發是一種非常常用的開發模式,通常是一個非常小的核心模塊加各種各樣的功能插件。如果你使用過一些開源 CMS 的話,肯定會用過其中的的插件功能,用戶可以通過啟用或者上傳插件包的方式動態添加一些功能,那麼在 Asp.NET Core 中如何實現插件化開發呢。今天我們推薦一款國人開發的輕量級插件框架 PluginCore

  • 簡單 - 約定優於配置, 以最少的配置幫助你專注於業務
  • 開箱即用 - 前後端自動集成, 兩行代碼完成集成
  • 動態 WebAPI - 每個插件都可新增 Controller, 擁有自己的路由
  • 插件前後端分離 - 可在插件 wwwroot 文件夾下放置前端文件 (index.html,...), 然後訪問 /plugins/pluginId/index.html
  • 熱插拔 - 上傳、安裝、啟用、禁用、卸載、刪除 均無需重啟站點; 甚至可通過插件在運行時添加 HTTP request pipeline middleware, 也無需重啟站點
  • 依賴注入 - 可在 實現 IPlugin 的插件類的構造方法上申請依賴注入項, 當然 Controller 構造方法上也可依賴注入
  • 易擴展 - 你可以編寫你自己的插件sdk, 然後引用插件sdk, 編寫擴展插件 - 自定義插件鉤子, 並應用
  • 掛件 - 你可在前端埋擴展點, 然後通過插件插入掛件
  • 無需資料庫 - 無資料庫依賴
  • 0侵入 - 近乎0侵入, 不影響你的現有系統
  • 極少依賴 - 只依賴於一個第三方包 ( 用於解壓的 SharpZipLib )

安裝及啟用

推薦通過 NuGet 安裝 PluginCore :PM> Install-Package PluginCore.AspNetCore。安裝完成後,可在 Asp.net Core 項目以如下方式啟用。

// Starup.cs

using PluginCore.AspNetCore.Extensions;

// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();

    // 1. 添加 PluginCore
    services.AddPluginCore();
}

// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }

    app.UseHttpsRedirection();

    app.UseRouting();

    // 2. 使用 PluginCore
    app.UsePluginCore();

    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllers();
    });
}

運行程序,即可通過 https://localhost:5001/PluginCore/Admin 訪問一個自帶的建議插件管理界面。當然,這作用不大,僅作為示例。

編寫插件

安裝插件模板

dotnet new --install PluginCore.Template

創建插件項目

dotnet new plugincore -n MyFirstPlugin

添加 HelloWorldPlugin 類繼承 BasePlugin,通過預先定義框架行為鉤子,插件再實現接口,將插件行為加入框架,如實現 IStartupXPlugin

支持插件 構造器注入 框架預先注入的服務等

public class HelloWorldPlugin : BasePlugin, IStartupXPlugin
{
    public override (bool IsSuccess, string Message) AfterEnable()
    {
        Console.WriteLine($"{nameof(HelloWorldPlugin)}: {nameof(AfterEnable)}");
        return base.AfterEnable();
    }

    public override (bool IsSuccess, string Message) BeforeDisable()
    {
        Console.WriteLine($"{nameof(HelloWorldPlugin)}: {nameof(BeforeDisable)}");
        return base.BeforeDisable();
    }

    public void ConfigureServices(IServiceCollection services)
    {

    }

    public void Configure(IApplicationBuilder app)
    {
        app.UseMiddleware<SayHelloMiddleware>();
    }

    public int ConfigureOrder => 2

    public int ConfigureServicesOrder => 2;
}

SayHelloMiddleware.cs

public class SayHelloMiddleware
{
    private readonly RequestDelegate _next;

    /// <summary>
    /// 在 <see cref="PluginApplicationBuilder"/> Build 時, 將會 new Middleware(), 最終將所有 Middleware 包裝為一個 <see cref="RequestDelegate"/>
    /// </summary>
    /// <param name="next"></param>
    public SayHelloMiddleware(RequestDelegate next)
    {
        _next = next;
    }


    /// <summary>
    /// 
    /// </summary>
    /// <param name="httpContext"></param>
    /// <param name="pluginFinder">測試,是否運行時添加的Middleware,是否可以依賴注入</param>
    /// <returns></returns>
    public async Task InvokeAsync(HttpContext httpContext, IPluginFinder pluginFinder)
    {
        // 測試: 成功
        List<IPlugin> plugins = pluginFinder.EnablePlugins()?.ToList();

        bool isMatch = false;

        isMatch = httpContext.Request.Path.Value.StartsWith("/SayHello");

        if (isMatch)
        {
            await httpContext.Response.WriteAsync($"Hello World! {DateTime.Now:yyyy-MM-dd HH:mm:ss} <br>" +
                                                  $"{httpContext.Request.Path} <br>" +
                                                  $"{httpContext.Request.QueryString.Value}", Encoding.UTF8);
        }
        else
        {
            // Call the next delegate/middleware in the pipeline
            await _next(httpContext);
        }
    }
}

其他配置

支持動態擴展 WebAPI,和普通WebAPI 項目相同,直接創建 Controller 即可注意: 這裡的 IUserInfoService 是集成了 PluginCore 的項目里的服務接口

[Route("api/plugins/[controller]")]
[ApiController]
public class UserHelloController : ControllerBase
{
    private readonly IUserInfoService _userInfoService;

    public UserHelloController(IUserInfoService userInfoService)
    {
        this._userInfoService = userInfoService;
    }

    public ActionResult Get()
    {
        UserInfo userInfo = _userInfoService.FirstOrDefaultAsync(m => !m.IsDeleted).Result;
        SettingsModel settingsModel = PluginSettingsModelFactory.Create<SettingsModel>("HelloWorldPlugin");
        string rtn = $"用戶名: {userInfo.UserName}, 創建時間: {userInfo.CreateTime.ToString()}, Hello: {settingsModel.Hello}";
        return Ok(rtn);
    }
}

插件設置(可選), Json Model 類繼承 PluginSettingsModel

public class SettingsModel : PluginSettingsModel
{
    public string Hello { get; set; }
}

文件名必須 settings.json

{
        "Hello": "哈哈哈哈哈或或或或或或" 
}

插件描述 info.json (必需)

{
        "PluginId": "HelloWorldPlugin",
        "DisplayName": "獲取一個用戶",
        "Description": "這是一個示例插件2號。",
        "Author": "yiyun",
        "Version": "0.1.0",
        "SupportedVersions": [ "0.0.1" ]
}

插件文檔 README.md(可選)

## 說明文檔(可選)

- [] 這是一個示例插件
- [x] 感謝使用

插件發布打包

右鍵選擇插件項目,點擊發布(Publish),再將發布後的插件文件夾打包為 xxx.zip 即可。壓縮包名可隨意,框架將以 info.json 中 PluginId 作為插件標識注意: PluginId 一定要與程序集名相同, 例如 PluginId 為 HelloWorldPlugin, 那麼最後打包里一定有 HelloWorldPlugin.dll。打包後的插件,即可通過 上傳本地插件 載入框架

HelloWorldPlugin.csproj 參考

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>netstandard2.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.2.0" />
  </ItemGroup>

  <ItemGroup>
    <PackageReference Include="PluginCore.IPlugins" Version="0.4.0" />
  </ItemGroup>

  <!-- 發布插件相關文件 -->
  <ItemGroup>
    <Content Include="info.json">
      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
    </Content>
    <Content Include="README.md">
      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
    </Content>
    <Content Include="settings.json">
      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
    </Content>
  </ItemGroup>

  <!-- 發布 wwwroot -->
  <ItemGroup>
    <Content Include="wwwroot\*">
      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
    </Content>
    <Content Include="wwwroot\*\*">
      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
    </Content>
  </ItemGroup>

</Project>

說實話,我自己還沒用過,但看介紹是挺好的,想在下一個項目里試試。 大家有興趣可以試試,項目地址:https://github.com/yiyungent/PluginCore

關鍵字: