簡介
插件化開發是一種非常常用的開發模式,通常是一個非常小的核心模塊加各種各樣的功能插件。如果你使用過一些開源 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