修改 CloudProduct ImageCache 项目改为 插件
This commit is contained in:
1 parent
5ff52dea92
commit
292cb8638c
26 files changed
+417
-282
No files matched your search
+21
-50
@@ -1,55 +1,30 @@
|
||||
using MAX.Plugins;
|
||||
using Hangfire;
|
||||
using Hangfire.MySql;
|
||||
using HangfireBasicAuthenticationFilter;
|
||||
using MAX.Core.Interfaces;
|
||||
using MAX.Framework.Interfaces;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using MySql.Data.MySqlClient;
|
||||
using Store.Modules.CloudProduct.Services;
|
||||
|
||||
namespace Store.Modules.CloudProduct
|
||||
{
|
||||
|
||||
public class CloudProductModule : IModule
|
||||
public sealed class CloudProductPlugin : PluginBase
|
||||
{
|
||||
/// <summary>
|
||||
/// 本模块自己的配置文件
|
||||
/// </summary>
|
||||
public const string ConfigurationFileName = "appsettings.CloudProduct.json";
|
||||
|
||||
public int Priority => IntPriority.Normal;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 服务注册阶段(builder.Build() 之前):这里只能拿到容器和配置,碰不到 app
|
||||
/// </summary>
|
||||
public void AddModule(IHostApplicationBuilder host)
|
||||
{
|
||||
IServiceCollection services = host.Services;
|
||||
IConfigurationManager configuration = host.Configuration;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(ConfigurationFileName))
|
||||
{
|
||||
string path = Path.IsPathRooted(ConfigurationFileName)? ConfigurationFileName: Path.Combine(Path.GetDirectoryName(GetType().Assembly.Location)!, ConfigurationFileName);
|
||||
configuration.AddJsonFile(path, optional: false, reloadOnChange: true);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
//// 本模块的配置节(Search / RawData / Indexing / Delete)都在模块自己的配置文件里,
|
||||
//// 关键项缺失直接报错,避免 OpenSearch 地址为空、原始数据写到意外目录之类的隐性故障
|
||||
RequireConfiguration(configuration, "Search:Url");
|
||||
RequireConfiguration(configuration, "Search:IndexName");
|
||||
RequireConfiguration(configuration, "Search:FavoritesIndexName");
|
||||
RequireConfiguration(configuration, "RawData:Path");
|
||||
public override string Id => "CloudProduct";
|
||||
protected override void RegisterServices(IServiceCollection services)
|
||||
{
|
||||
var configuration = Context.Configuration;
|
||||
RequireConfiguration("Search:Url");
|
||||
RequireConfiguration("Search:IndexName");
|
||||
RequireConfiguration("Search:FavoritesIndexName");
|
||||
RequireConfiguration("RawData:Path");
|
||||
|
||||
// ===== Hangfire 后台定时任务(商品分词同步) =====
|
||||
var hangfireConnection = configuration.GetValue<string>("CloudProductConnection")
|
||||
?? throw new InvalidOperationException("缺少配置 ConnectionStrings:DefaultConnection,CloudProduct 模块的 Hangfire 无法启动。");
|
||||
?? throw new InvalidOperationException("缺少配置 CloudProductConnection,CloudProduct 模块的 Hangfire 无法启动。");
|
||||
|
||||
// AllowUserVariables=true:Hangfire.MySqlStorage 的建表/查询语句依赖用户变量
|
||||
var connectionStringBuilder = new MySqlConnectionStringBuilder(hangfireConnection)
|
||||
@@ -64,17 +39,22 @@ namespace Store.Modules.CloudProduct
|
||||
.UseStorage(new MySqlStorage(connectionStringBuilder.ConnectionString, new MySqlStorageOptions() { TablesPrefix = "Hangfire_" })));
|
||||
services.AddHangfireServer();
|
||||
services.AddTransient<GoodsIndexJob>();
|
||||
var connection = RequireConfiguration("CloudProductConnection");
|
||||
services.AddKeyedSingleton<IFreeSql>("CloudProductConnection", (_, _) =>
|
||||
new FreeSql.FreeSqlBuilder().UseConnectionString(FreeSql.DataType.MySql, connection)
|
||||
.UseAdoConnectionPool(true).Build());
|
||||
services.AddHostedService<CloudProductSearchStartup>();
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 管道装配阶段(builder.Build() 之后、app.Run() 之前):这里可以挂中间件、解析容器里的服务
|
||||
/// </summary>
|
||||
public void UseModule(IApplicationBuilder app)
|
||||
protected override void ConfigureApplication(IApplicationBuilder app)
|
||||
{
|
||||
try
|
||||
{
|
||||
var Configuration = app.ApplicationServices.GetService<IConfiguration>()!;
|
||||
var Configuration = Context.Configuration;
|
||||
// Hangfire 后台任务看板(默认地址 /hangfire)
|
||||
app.UseHangfireDashboard("/hangfire", new DashboardOptions
|
||||
{
|
||||
@@ -104,15 +84,6 @@ namespace Store.Modules.CloudProduct
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 校验模块配置项存在(值来自模块自己的 <see cref="ConfigurationFileName"/>)
|
||||
/// </summary>
|
||||
private static void RequireConfiguration(IConfiguration configuration, string key)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(configuration.GetValue<string>(key)))
|
||||
{
|
||||
throw new InvalidOperationException($"缺少配置 {key},请检查模块配置文件 {ConfigurationFileName}。");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Store.Modules.CloudProduct.Services;
|
||||
|
||||
/// <summary>查询搜索服务状态;网络不可用时记录错误,不阻止 HTTP 服务启动。</summary>
|
||||
internal sealed class CloudProductSearchStartup(
|
||||
IOpenSearchService search, ILogger<CloudProductSearchStartup> logger) : BackgroundService
|
||||
{
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
await search.GetStatusAsync().WaitAsync(stoppingToken);
|
||||
}
|
||||
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested) { }
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogWarning(ex, "CloudProduct 搜索服务启动检查失败。");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using MAX.Plugins;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Store.Modules.CloudProduct.Services
|
||||
@@ -13,10 +14,10 @@ namespace Store.Modules.CloudProduct.Services
|
||||
private readonly IConfiguration _configuration;
|
||||
private readonly ILogger<GoodsIndexJob> _logger;
|
||||
|
||||
public GoodsIndexJob(IServiceScopeFactory scopeFactory, IConfiguration configuration,ILogger<GoodsIndexJob> logger)
|
||||
public GoodsIndexJob(IServiceScopeFactory scopeFactory, [FromKeyedServices("CloudProduct")] PluginContext context,ILogger<GoodsIndexJob> logger)
|
||||
{
|
||||
_scopeFactory = scopeFactory;
|
||||
_configuration = configuration;
|
||||
_configuration = context.Configuration;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using MAX.Plugins;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using MAX.Core.DependencyInjection;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Store.Modules.CloudProduct.Dtos;
|
||||
@@ -24,12 +26,12 @@ namespace Store.Modules.CloudProduct.Services
|
||||
private readonly IRawDataStorage _rawDataStorage;
|
||||
private readonly IConfiguration _configuration;
|
||||
|
||||
public GoodsService(GoodsRepository repository, IOpenSearchService searchService, IRawDataStorage rawDataStorage, IConfiguration configuration)
|
||||
public GoodsService(GoodsRepository repository, IOpenSearchService searchService, IRawDataStorage rawDataStorage, [FromKeyedServices("CloudProduct")] PluginContext context)
|
||||
{
|
||||
_repository = repository;
|
||||
_searchService = searchService;
|
||||
_rawDataStorage = rawDataStorage;
|
||||
_configuration = configuration;
|
||||
_configuration = context.Configuration;
|
||||
}
|
||||
|
||||
public async Task<(List<GoodsEntity> List, long Total)> SearchGoodsAsync(GoodsSearchRequest request)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using MAX.Plugins;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using System.Diagnostics;
|
||||
using MAX.Core.DependencyInjection;
|
||||
using MAX.Framework.Commons;
|
||||
@@ -40,9 +42,9 @@ namespace Store.Modules.CloudProduct.Services
|
||||
private static readonly TimeSpan AdminTimeout = TimeSpan.FromSeconds(30);
|
||||
private readonly ILogger<OpenSearchService> _logger;
|
||||
|
||||
public OpenSearchService(IConfiguration configuration,ILogger<OpenSearchService> logger)
|
||||
public OpenSearchService([FromKeyedServices("CloudProduct")] PluginContext context,ILogger<OpenSearchService> logger)
|
||||
{
|
||||
var config = configuration.GetSection("Search");
|
||||
var config = context.Configuration.GetSection("Search");
|
||||
_nodeUrl = config.GetValue<string>("Url") ?? "";
|
||||
_indexName = config.GetValue<string>("IndexName") ?? "";
|
||||
_client = CreateClient();
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using MAX.Plugins;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using MAX.Core.DependencyInjection;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using System.IO.Compression;
|
||||
@@ -30,13 +32,13 @@ namespace Store.Modules.CloudProduct.Services
|
||||
{
|
||||
private readonly string _rootPath;
|
||||
|
||||
public RawDataStorage(IConfiguration configuration)
|
||||
public RawDataStorage([FromKeyedServices("CloudProduct")] PluginContext context)
|
||||
{
|
||||
var configured = configuration.GetValue<string>("RawData:Path");
|
||||
var configured = context.Configuration.GetValue<string>("RawData:Path");
|
||||
if (string.IsNullOrWhiteSpace(configured))
|
||||
configured = "RawData";
|
||||
|
||||
_rootPath = Path.GetFullPath(configured);
|
||||
_rootPath = Path.GetFullPath(configured, context.DirectoryPath);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using MAX.Plugins;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using MAX.Core.DependencyInjection;
|
||||
using MAX.Framework.Commons;
|
||||
using MAX.Framework.Dtos;
|
||||
@@ -53,9 +55,9 @@ namespace Store.Modules.CloudProduct.Services
|
||||
// 批量写入/建索引/删除索引等管理类操作超时(数据量大时需要更长)
|
||||
private static readonly TimeSpan AdminTimeout = TimeSpan.FromSeconds(30);
|
||||
|
||||
public UserFavoritesIndexService(IConfiguration configuration, ILogger<UserFavoritesIndexService> logger)
|
||||
public UserFavoritesIndexService([FromKeyedServices("CloudProduct")] PluginContext context, ILogger<UserFavoritesIndexService> logger)
|
||||
{
|
||||
var config = configuration.GetSection("Search");
|
||||
var config = context.Configuration.GetSection("Search");
|
||||
_nodeUrl = config.GetValue<string>("Url") ?? "";
|
||||
_indexName = config.GetValue<string>("FavoritesIndexName") ?? "";
|
||||
_logger = logger;
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
using MAX.Core.DependencyInjection;
|
||||
using MAX.Core.Interfaces;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Store.Modules.CloudProduct.Services;
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace Store.Modules.CloudProduct
|
||||
{
|
||||
|
||||
public class Startup : IStartup
|
||||
{
|
||||
public int Priority => IntPriority.Normal;
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
}
|
||||
|
||||
public void Register(IServiceRegistrar registrar)
|
||||
{
|
||||
|
||||
|
||||
|
||||
Func<IServiceProvider, IFreeSql> KJDFsqlFactory = r =>
|
||||
{
|
||||
var config = r.GetRequiredService<IConfiguration>();
|
||||
var connection = config.GetValue<string>("CloudProductConnection");
|
||||
IFreeSql fsql = new FreeSql.FreeSqlBuilder()
|
||||
.UseConnectionString(FreeSql.DataType.MySql, connection)
|
||||
.UseAdoConnectionPool(true)
|
||||
.Build();
|
||||
return fsql;
|
||||
};
|
||||
registrar.RegisterSingleton<IFreeSql>("CloudProductConnection", (s, k) =>
|
||||
{
|
||||
return KJDFsqlFactory(s);
|
||||
});
|
||||
}
|
||||
|
||||
public async void Start(IServiceProvider serviceProvider)
|
||||
{
|
||||
var openSearchService = serviceProvider.GetRequiredService<OpenSearchService>();
|
||||
await openSearchService.GetStatusAsync();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -10,16 +10,12 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Store.Framework\Store.Framework.csproj" />
|
||||
<ProjectReference Include="..\..\Core\MAX.Plugins\MAX.Plugins.csproj" />
|
||||
</ItemGroup>
|
||||
<!--<ItemGroup>
|
||||
--><!-- 本模块自己的配置文件:源码里叫 appsettings.json,构建后复制到宿主输出目录时改名为
|
||||
appsettings.CloudProduct.json,避免和宿主自己的 appsettings.json 互相覆盖。
|
||||
由 builder.AddStoreHub() 自动并入宿主配置树(在 appsettings*.json 之后、环境变量之前)。 --><!--
|
||||
<None Update="appsettings.json" Link="appsettings.CloudProduct.json" CopyToOutputDirectory="PreserveNewest" />
|
||||
</ItemGroup>-->
|
||||
|
||||
|
||||
<ItemGroup>
|
||||
<None Update="appsettings.CloudProduct.json">
|
||||
<None Update="appsettings*.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
|
||||
</None>
|
||||
|
||||
File renamed without changes.
+57
-51
@@ -1,86 +1,92 @@
|
||||
# 独立功能插件
|
||||
|
||||
`Modules/` 放置商城业务模块,`Plugins/` 放置共用 API 宿主的独立功能。
|
||||
插件通过 IModule、IStartup 和依赖标记接口接入现有生命周期,控制器共用宿主 URL。
|
||||
`Modules/` 使用 IModule,存放商城业务模块;`Plugins/` 使用 IPlugin,存放共用 API 宿主的独立功能。
|
||||
公共项目 `Core/MAX.Plugins` 不依赖商城业务或数据库库,提供:
|
||||
|
||||
## 发布和运行
|
||||
- `IPlugin`:唯一 Id、Priority、服务注册和管道装配生命周期。
|
||||
- `PluginBase`:保存插件上下文、配置校验、默认管道实现;派生类只需实现 RegisterServices,按需重写 ConfigureApplication。
|
||||
- `PluginContext`:插件目录、宿主环境、隔离的 Configuration 和必需配置校验。
|
||||
|
||||
在仓库根目录执行生成命令:
|
||||
宿主扫描加载插件,按 Priority 降序注册,显式注册 MVC Application Parts;IModule/IStartup 与 IPlugin 分开执行,禁止同一入口同时实现两套生命周期。
|
||||
|
||||
## 生成和部署
|
||||
|
||||
在仓库根目录执行,宿主无需引用具体插件:
|
||||
|
||||
```powershell
|
||||
dotnet build Store.API/Store.Api.csproj -c Release
|
||||
dotnet build Plugins/CloudProduct/Store.CloudProduct.csproj -c Release
|
||||
dotnet build Plugins/Store.ImageCache/Store.ImageCache.csproj -c Release
|
||||
dotnet Bin/Store.Api.dll
|
||||
dotnet Bin/Store.Api.dll --contentRoot "$PWD/Bin"
|
||||
```
|
||||
|
||||
API 直接生成到仓库根目录 `Bin/`,不再附加配置名称、目标框架或运行时子目录。
|
||||
各插件生成后自动将自身完整输出(DLL、deps.json、配置、运行依赖及 runtimes 子目录)复制到
|
||||
`Bin/Plugins/<项目名>/`。例如:
|
||||
API 输出到根目录 Bin;插件生成后自动复制到 Bin/Plugins/<项目名>/。生成过程按相对路径和 SHA-256 清理宿主已提供的重复文件;插件自己的 appsettings*.json 始终保留。
|
||||
宿主后生成时也会去重;同名但内容不同的依赖保留,由加载器检查版本兼容性。
|
||||
Debug/Release 共用 Bin,部署前请统一使用 Release。整体拷贝 Bin 部署,需要兼容的 .NET/ASP.NET Core 运行时。
|
||||
|
||||
```text
|
||||
Bin/
|
||||
Store.Api.dll
|
||||
Store.Api.deps.json
|
||||
Store.Api.runtimeconfig.json
|
||||
MAX.Plugins.dll
|
||||
appsettings.json
|
||||
Plugins/
|
||||
Store.CloudProduct/
|
||||
Store.CloudProduct.dll
|
||||
Store.CloudProduct.deps.json
|
||||
appsettings.json
|
||||
Store.ImageCache/
|
||||
Store.ImageCache.dll
|
||||
Store.ImageCache.deps.json
|
||||
appsettings.json
|
||||
```
|
||||
|
||||
生成全部所需项目后即可整体拷贝 `Bin/` 部署,目标机器需要兼容的 .NET/ASP.NET Core 运行时。
|
||||
API 不引用插件,也不会在单独生成 API 时触发插件构建;生成插件项目或生成包含插件的解决方案时会自动复制。
|
||||
未来在 Plugins 下新增项目会继承同目录的 Directory.Build.props/targets,无需逐个添加复制任务。
|
||||
每个部署子目录只放一个插件入口。构建会清理重命名前的旧入口文件;其他过期依赖或旧目录需要自行清理。
|
||||
插件扫描配置 Plugins:Path 默认 Plugins,相对 AppContext.BaseDirectory,也支持绝对路径;可通过 Plugins__Path 覆盖。
|
||||
MSBuild 属性 StoreHostOutputPath 可覆盖生成目录,应使用以分隔符结尾的绝对路径。
|
||||
插件仅启动时加载,更新后重启,不支持卸载、热更新、进程隔离或冲突依赖版本隔离。
|
||||
|
||||
Debug 与 Release 共用此目录;部署前应以相同配置重新生成宿主及所需插件。
|
||||
生成宿主或插件后,都会按相对路径和 SHA-256 清理与宿主完全相同的插件文件;同名但内容不同的文件保留。
|
||||
宿主尚未生成时插件暂时保留完整依赖,之后生成宿主会自动去重。去重后的插件目录必须与匹配的宿主一起部署。
|
||||
同一插件输出和部署子目录中的旧入口(其他名称的 .deps.json 及对应 DLL、PDB、runtimeconfig.json)会在生成时自动清理,避免重复加载重命名前的插件。每个部署子目录只放一个插件入口。
|
||||
其他旧文件不会自动删除;插件部署目录更名、删除或移除依赖后,应清理对应旧目录再生成。
|
||||
MSBuild 属性 `StoreHostOutputPath` 可覆盖此路径,覆盖时使用绝对目录并以目录分隔符结尾。
|
||||
## 配置隔离
|
||||
|
||||
需要标准 publish 产物时也可以指定独立部署目录:
|
||||
各插件只读取自身目录,按以下顺序覆盖:
|
||||
|
||||
```powershell
|
||||
dotnet publish Store.API/Store.Api.csproj -c Release -o artifacts/server
|
||||
dotnet publish Plugins/CloudProduct/Store.CloudProduct.csproj -c Release -o artifacts/server/Plugins/Store.CloudProduct
|
||||
dotnet publish Plugins/Store.ImageCache/Store.ImageCache.csproj -c Release -o artifacts/server/Plugins/Store.ImageCache
|
||||
1. appsettings.json(必需)。
|
||||
2. appsettings.<EnvironmentName>.json(可选,例如 appsettings.Development.json)。
|
||||
3. 前缀 Plugins__<Id>__ 的环境变量,例如 Plugins__CloudProduct__Search__Url。
|
||||
|
||||
宿主配置、其他插件配置不会并入插件配置,插件配置也不会修改宿主 IConfiguration。
|
||||
配置在启动时加载,修改后重启。缺失文件、格式错误或缺失必需项时,错误包含插件身份及上下文信息。
|
||||
原有配置键保持不变;原始数据相对路径改为相对于 CloudProduct 插件目录,绝对路径不变。
|
||||
|
||||
插件通过 Context.Configuration 读取配置。业务服务可使用按插件 Id 注册的上下文,或在插件注册时将配置绑定到专属 Options:
|
||||
|
||||
```csharp
|
||||
public ExampleService([FromKeyedServices("CloudProduct")] PluginContext context)
|
||||
{
|
||||
var url = context.RequireConfiguration("Search:Url");
|
||||
}
|
||||
```
|
||||
|
||||
宿主配置 `Plugins:Path` 默认为 `Plugins`,相对路径以宿主二进制目录为基准,也可使用绝对路径。
|
||||
可以通过环境变量 `Plugins__Path` 覆盖。目录不存在或为空时不加载插件。
|
||||
数据库、Hangfire、OpenSearch 保留在各自插件内;认证、日志等仍由宿主管理。
|
||||
CloudProduct 的搜索启动检查使用 BackgroundService,网络错误记录日志,不阻断 HTTP 启动;Hangfire 仍按既有配置运行。
|
||||
|
||||
每个插件子目录包含入口 DLL、同名 `.deps.json`、配置和宿主未提供的依赖;共享依赖由宿主提供。
|
||||
上面的标准 `dotnet publish -o` 命令仍生成完整独立发布内容,仅 `Bin/Plugins/` 自动执行宿主去重。
|
||||
加载器递归寻找 `.deps.json`,由 `StoreHubAppTypeFinder.cs` 中现有 Match/AvailablePrefix 过滤入口程序集。
|
||||
依赖解析不受入口名称过滤限制。两个现有插件从自身程序集所在目录读取 JSON 配置。
|
||||
## 新增插件
|
||||
|
||||
## 新增与更新
|
||||
在 Plugins 下创建类库,引用 Core/MAX.Plugins 和需要的共享业务框架,提供有无参构造函数的 PluginBase 派生类以及唯一 Id。
|
||||
提供 appsettings.json,将 appsettings*.json 设置为复制到生成和发布目录。Plugins 下的公共构建配置自动携带依赖并复制到宿主目录。
|
||||
无需修改 Store.API;入口程序集名称需符合 StoreHubAppTypeFinder.cs 中的过滤规则。
|
||||
|
||||
新增插件无需修改 Store.API:创建类库,引用共享框架,实现需要的模块/启动接口,启用
|
||||
`EnableDynamicLoading`,并将配置设置为复制到输出和发布目录;生成后会自动复制到宿主扫描目录;也可以独立发布到其他扫描目录。
|
||||
配置文件命名与配置节应避免和其他插件冲突。现有模块的配置合并顺序保持不变。
|
||||
如需完整独立发布产物,可分别 dotnet publish -o 到自定义目录;自定义发布目录不执行 Bin 下的共享文件去重。
|
||||
|
||||
插件、宿主和共享契约必须使用兼容版本。所有程序集加载到默认上下文,同名程序集只加载一次;
|
||||
发布目录中同名依赖身份不一致会在加载时明确报错,不提供依赖版本隔离。
|
||||
插件必需的程序集、类型或配置缺失时启动失败;未调用的第三方可选功能依赖可能在调用时才解析。
|
||||
|
||||
插件与宿主同进程运行,共用认证、容器和中间件。更新时替换完整发布目录并重启宿主;
|
||||
不支持热加载、卸载和进程隔离。CloudProduct 仍需要其数据库、OpenSearch 和后台任务配置。
|
||||
|
||||
## 加载冒烟验证
|
||||
|
||||
验证程序不连接数据库或 OpenSearch;每个场景单独启动进程:
|
||||
## 验证
|
||||
|
||||
```powershell
|
||||
dotnet run --project Tests/PluginLoadingSmoke -- <插件根目录>
|
||||
dotnet run --project Tests/PluginLoadingSmoke -- <仅包含ImageCache的发布目录> image
|
||||
dotnet run --project Tests/PluginLoadingSmoke -- configuration
|
||||
dotnet run --project Tests/PluginLoadingSmoke -- "$PWD/Bin/Plugins"
|
||||
dotnet run --project Tests/PluginLoadingSmoke -- "$PWD/Bin/Plugins/Store.ImageCache" image
|
||||
dotnet run --project Tests/PluginLoadingSmoke -- <空目录或不存在目录> empty
|
||||
dotnet run --project Tests/PluginLoadingSmoke -- - empty
|
||||
dotnet run --project Tests/PluginLoadingSmoke -- <缺依赖或包含版本冲突的目录> failure
|
||||
dotnet run --project Tests/PluginLoadingSmoke -- <缺依赖或版本冲突目录> failure
|
||||
```
|
||||
|
||||
`-` 使用默认 Plugins 路径;默认路径下已部署插件时使用普通模式。
|
||||
普通模式验证两个插件及控制器发现和程序集去重;`image` 额外验证真实框架注册流程中的配置、DI、IStartup 和 MVC。
|
||||
将同一插件发布内容复制到另一子目录可验证重复程序集;缺失依赖和版本冲突应报告具体插件路径。
|
||||
配置验证覆盖同名键隔离、环境文件和环境变量覆盖、必需文件和配置项缺失。
|
||||
加载验证覆盖两个插件的控制器、DI、数据库服务注册和业务服务构造,不连接数据库或启动后台任务。
|
||||
真实启动还需数据库和 OpenSearch;外部服务不可达应结合日志判断对应功能状态。
|
||||
@@ -0,0 +1,17 @@
|
||||
using MAX.Plugins;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace Store.ImageCache;
|
||||
|
||||
public sealed class ImageCachePlugin : PluginBase
|
||||
{
|
||||
public override string Id => "ImageCache";
|
||||
protected override void RegisterServices(IServiceCollection services)
|
||||
{
|
||||
var connection = RequireConfiguration("ImageCache:ConnectionString");
|
||||
services.AddKeyedSingleton<IFreeSql>("ImageCacheConnection", (_, _) =>
|
||||
new FreeSql.FreeSqlBuilder().UseConnectionString(FreeSql.DataType.MySql, connection)
|
||||
.UseAdoConnectionPool(true).Build());
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
|
||||
using MAX.Core.Interfaces;
|
||||
using MAX.Framework.Interfaces;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using MySql.Data.MySqlClient;
|
||||
|
||||
namespace Store.Modules.CloudProduct
|
||||
{
|
||||
public class Module : IModule
|
||||
{
|
||||
/// <summary>
|
||||
/// 本模块自己的配置文件
|
||||
/// </summary>
|
||||
public const string ConfigurationFileName = "appsettings.ImageCache.json";
|
||||
|
||||
public int Priority => IntPriority.Normal;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 服务注册阶段(builder.Build() 之前):这里只能拿到容器和配置,碰不到 app
|
||||
/// </summary>
|
||||
public void AddModule(IHostApplicationBuilder host)
|
||||
{
|
||||
IServiceCollection services = host.Services;
|
||||
IConfigurationManager configuration = host.Configuration;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(ConfigurationFileName))
|
||||
{
|
||||
string path = Path.IsPathRooted(ConfigurationFileName)? ConfigurationFileName: Path.Combine(Path.GetDirectoryName(GetType().Assembly.Location)!, ConfigurationFileName);
|
||||
configuration.AddJsonFile(path, optional: false, reloadOnChange: true);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 管道装配阶段(builder.Build() 之后、app.Run() 之前):这里可以挂中间件、解析容器里的服务
|
||||
/// </summary>
|
||||
public void UseModule(IApplicationBuilder app)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
using MAX.Core.DependencyInjection;
|
||||
using MAX.Core.Interfaces;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace Store.Modules.CloudProduct
|
||||
{
|
||||
|
||||
public class Startup : IStartup
|
||||
{
|
||||
public int Priority => IntPriority.Normal;
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
}
|
||||
|
||||
public void Register(IServiceRegistrar registrar)
|
||||
{
|
||||
|
||||
|
||||
|
||||
Func<IServiceProvider, IFreeSql> KJDFsqlFactory = r =>
|
||||
{
|
||||
var config = r.GetRequiredService<IConfiguration>();
|
||||
var connection = config.GetValue<string>("ImageCache:ConnectionString");
|
||||
IFreeSql fsql = new FreeSql.FreeSqlBuilder()
|
||||
.UseConnectionString(FreeSql.DataType.MySql, connection)
|
||||
.UseAdoConnectionPool(true)
|
||||
.Build();
|
||||
return fsql;
|
||||
};
|
||||
registrar.RegisterSingleton<IFreeSql>("ImageCacheConnection", (s, k) =>
|
||||
{
|
||||
return KJDFsqlFactory(s);
|
||||
});
|
||||
}
|
||||
|
||||
public void Start(IServiceProvider serviceProvider)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,9 +9,10 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Store.Framework\Store.Framework.csproj" />
|
||||
<ProjectReference Include="..\..\Core\MAX.Plugins\MAX.Plugins.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Update="appsettings.ImageCache.json">
|
||||
<None Update="appsettings*.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
|
||||
</None>
|
||||
|
||||
File renamed without changes.
Reference in new issue
Block a user