修改 CloudProduct ImageCache 项目改为 插件
This commit is contained in:
1 parent
5ff52dea92
commit
292cb8638c
26 files changed
+417
-282
No files matched your search
@@ -13,6 +13,7 @@ using Microsoft.Win32;
|
||||
using System.Configuration;
|
||||
using System.Diagnostics;
|
||||
using System.Reflection;
|
||||
using MAX.Plugins;
|
||||
|
||||
namespace MAX.Framework.DependencyInjection
|
||||
{
|
||||
@@ -33,10 +34,13 @@ namespace MAX.Framework.DependencyInjection
|
||||
/// </summary>
|
||||
public static IServiceCollection AddStore(this IHostApplicationBuilder host)
|
||||
{
|
||||
host.AddLogger();
|
||||
Debug.WriteLine("[Store] 开始注册应用服务");
|
||||
RunDebugStep("Logger", "注册日志服务", () => host.AddLogger());
|
||||
//host.Services.AddSingleton<ILoggerFacade>(NLogger.Instance);//注册log
|
||||
|
||||
IEnumerable<Assembly> assemblies = StoreAppTypeFinder.Current.GetAssemblies(host.Configuration["Plugins:Path"]).ToArray();
|
||||
foreach (var assembly in assemblies)
|
||||
Debug.WriteLine($"[Assembly] 已发现:{assembly.FullName}");
|
||||
// 同一个类可能同时实现 IStartup 与 IModule,这里缓存实例,保证两个阶段拿到的是同一个对象
|
||||
Dictionary<Type, object> instances = [];
|
||||
return AddStoreCore(host, assemblies, instances);
|
||||
@@ -54,14 +58,16 @@ namespace MAX.Framework.DependencyInjection
|
||||
IEnumerable<Assembly> assemblies,
|
||||
IDictionary<Type, object> instances)
|
||||
{
|
||||
RegisterDependencyMarkers(host.Services, assemblies);
|
||||
RunDebugStep("DI", "注册依赖标记服务", () => RegisterDependencyMarkers(host.Services, assemblies));
|
||||
|
||||
RunModules(host, assemblies, instances);
|
||||
RunDebugStep("Module", "注册全部模块", () => RunModules(host, assemblies, instances));
|
||||
RunDebugStep("Plugin", "注册全部插件", () => RunPlugins(host, assemblies, instances));
|
||||
|
||||
RunStartups(host.Services, assemblies, instances);
|
||||
RunDebugStep("Startup", "初始化并注册启动服务", () => RunStartups(host.Services, assemblies, instances));
|
||||
|
||||
|
||||
|
||||
Debug.WriteLine("[MVC] 开始注册控制器和全局过滤器");
|
||||
var mvc = host.Services.AddControllers(options =>
|
||||
{
|
||||
options.Filters.Add<GlobalExceptionFilter>();
|
||||
@@ -90,7 +96,9 @@ namespace MAX.Framework.DependencyInjection
|
||||
foreach (var assembly in assemblies)
|
||||
{
|
||||
mvc.AddApplicationPart(assembly);
|
||||
Debug.WriteLine($"[MVC] 已添加 ApplicationPart:{assembly.GetName().Name}");
|
||||
}
|
||||
Debug.WriteLine($"[Store] 应用服务注册完成,共 {host.Services.Count} 个服务描述");
|
||||
return host.Services;
|
||||
}
|
||||
|
||||
@@ -101,7 +109,6 @@ namespace MAX.Framework.DependencyInjection
|
||||
{
|
||||
foreach (Type type in GetConcreteTypes(assemblies))
|
||||
{
|
||||
Debug.WriteLine(type.Namespace);
|
||||
Type? marker = DependencyMarkers.FirstOrDefault(m => m.IsAssignableFrom(type));
|
||||
if (marker is null)
|
||||
{
|
||||
@@ -114,6 +121,7 @@ namespace MAX.Framework.DependencyInjection
|
||||
|
||||
// 注册为自身
|
||||
services.Add(new ServiceDescriptor(type, type, lifetime));
|
||||
Debug.WriteLine($"[DI] 注册自身:{type.FullName},Lifetime={lifetime}");
|
||||
|
||||
// 注册为实现的接口(排除标记接口与系统接口)
|
||||
foreach (Type iface in type.GetInterfaces())
|
||||
@@ -124,6 +132,7 @@ namespace MAX.Framework.DependencyInjection
|
||||
}
|
||||
|
||||
services.Add(new ServiceDescriptor(iface, type, lifetime));
|
||||
Debug.WriteLine($"[DI] 注册接口:{iface.FullName} -> {type.FullName},Lifetime={lifetime}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -134,11 +143,11 @@ namespace MAX.Framework.DependencyInjection
|
||||
private static void RunStartups(IServiceCollection services, IEnumerable<Assembly> assemblies, IDictionary<Type, object> instances)
|
||||
{
|
||||
IServiceRegistrar registrar = new ServiceRegistrarProxy(services);
|
||||
Startups = CreateInstances<IStartup>(assemblies, instances).OrderByDescending(s => s.Priority);
|
||||
Startups = CreateInstances<IStartup>(assemblies, instances).Where(s => s is not IPlugin).OrderByDescending(s => s.Priority);
|
||||
foreach (IStartup startup in Startups)
|
||||
{
|
||||
startup.Initialize();
|
||||
startup.Register(registrar);
|
||||
RunDebugStep("Startup", $"初始化 {startup.GetType().FullName},Priority={startup.Priority}", startup.Initialize);
|
||||
RunDebugStep("Startup", $"注册服务 {startup.GetType().FullName}", () => startup.Register(registrar));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,12 +157,12 @@ namespace MAX.Framework.DependencyInjection
|
||||
/// </summary>
|
||||
private static void RunModules(IHostApplicationBuilder host, IEnumerable<Assembly> assemblies, IDictionary<Type, object> instances)
|
||||
{
|
||||
Modules = CreateInstances<IModule>(assemblies, instances).OrderByDescending(s => s.Priority);
|
||||
Modules = CreateInstances<IModule>(assemblies, instances).Where(m => m is not IPlugin).OrderByDescending(s => s.Priority);
|
||||
foreach (IModule module in Modules)
|
||||
{
|
||||
try
|
||||
{
|
||||
module.AddModule(host);
|
||||
RunDebugStep("Module", $"注册 {module.GetType().FullName},Priority={module.Priority}", () => module.AddModule(host));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -175,6 +184,11 @@ namespace MAX.Framework.DependencyInjection
|
||||
instance = Activator.CreateInstance(type, true)
|
||||
?? throw new InvalidOperationException($"无法创建 {type.FullName} 的实例(需要有参数量为 0 的构造函数)。");
|
||||
instances[type] = instance;
|
||||
Debug.WriteLine($"[Instance] 已创建:{type.FullName}");
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.WriteLine($"[Instance] 复用实例:{type.FullName}");
|
||||
}
|
||||
|
||||
yield return (T)instance;
|
||||
@@ -196,6 +210,9 @@ namespace MAX.Framework.DependencyInjection
|
||||
}
|
||||
catch (ReflectionTypeLoadException ex)
|
||||
{
|
||||
Debug.WriteLine($"[Assembly] 部分类型加载失败:{assembly.FullName}");
|
||||
foreach (var error in ex.LoaderExceptions)
|
||||
Debug.WriteLine($"[Assembly] 类型加载错误:{error?.Message}");
|
||||
return ex.Types.OfType<Type>();
|
||||
}
|
||||
}
|
||||
@@ -209,6 +226,53 @@ namespace MAX.Framework.DependencyInjection
|
||||
return StoreAppTypeFinder.Current.GetAssemblies();
|
||||
}
|
||||
|
||||
private sealed class PluginRuntime : IDisposable
|
||||
{
|
||||
public List<(IPlugin Plugin, PluginContext Context)> Entries { get; } = [];
|
||||
public void Dispose()
|
||||
{
|
||||
foreach (var entry in Entries) entry.Context.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private static void RunPlugins(IHostApplicationBuilder host, IEnumerable<Assembly> assemblies, IDictionary<Type, object> instances)
|
||||
{
|
||||
var runtime = new PluginRuntime();
|
||||
var ids = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
try
|
||||
{
|
||||
foreach (var plugin in CreateInstances<IPlugin>(assemblies, instances).OrderByDescending(p => p.Priority))
|
||||
{
|
||||
if (plugin is IModule || plugin is IStartup)
|
||||
throw new InvalidOperationException($"插件 {plugin.GetType().FullName} 不能同时实现 IModule/IStartup。");
|
||||
if (string.IsNullOrWhiteSpace(plugin.Id) || !ids.Add(plugin.Id))
|
||||
throw new InvalidOperationException($"插件标识为空或重复:{plugin.Id}");
|
||||
try
|
||||
{
|
||||
var directory = Path.GetDirectoryName(plugin.GetType().Assembly.Location)!;
|
||||
Debug.WriteLine($"[Plugin] 加载独立配置:{plugin.Id},目录={directory},环境={host.Environment.EnvironmentName}");
|
||||
var context = new PluginContext(plugin.Id, directory, host.Environment);
|
||||
runtime.Entries.Add((plugin, context));
|
||||
host.Services.AddKeyedSingleton<PluginContext>(plugin.Id, (provider, _) =>
|
||||
{
|
||||
// 只要上下文被解析,就让容器持有并管理全部插件配置的释放。
|
||||
provider.GetRequiredService<PluginRuntime>();
|
||||
return context;
|
||||
});
|
||||
RunDebugStep("Plugin", $"注册 {plugin.Id} ({plugin.GetType().FullName}),Priority={plugin.Priority}",
|
||||
() => plugin.ConfigureServices(host.Services, context));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"[Plugin] 注册失败:{plugin.Id},{ex.Message}");
|
||||
throw new InvalidOperationException($"插件 {plugin.Id} ({plugin.GetType().Assembly.Location}) 注册失败:{ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
host.Services.AddSingleton(_ => runtime);
|
||||
}
|
||||
catch { runtime.Dispose(); throw; }
|
||||
}
|
||||
|
||||
private static bool IsSystemInterface(Type type)
|
||||
{
|
||||
return type.Namespace is null
|
||||
@@ -217,18 +281,40 @@ namespace MAX.Framework.DependencyInjection
|
||||
}
|
||||
|
||||
|
||||
private static void RunDebugStep(string category, string description, Action action)
|
||||
{
|
||||
Debug.WriteLine($"[{category}] 开始:{description}");
|
||||
try
|
||||
{
|
||||
action();
|
||||
Debug.WriteLine($"[{category}] 完成:{description}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"[{category}] 失败:{description},{ex.Message}");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public static IApplicationBuilder UseStore(this IApplicationBuilder app)
|
||||
{
|
||||
app.UseMiddleware<UnifyStatusCodeMiddleware>();
|
||||
Debug.WriteLine("[Store] 开始装配应用管道");
|
||||
RunDebugStep("Middleware", "注册统一状态码中间件", () => app.UseMiddleware<UnifyStatusCodeMiddleware>());
|
||||
foreach (var (plugin, context) in app.ApplicationServices.GetRequiredService<PluginRuntime>().Entries)
|
||||
{
|
||||
try { RunDebugStep("Plugin", $"装配管道 {plugin.Id}", () => plugin.Configure(app, context)); }
|
||||
catch (Exception ex) { throw new InvalidOperationException($"插件 {plugin.Id} 管道装配失败:{ex.Message}", ex); }
|
||||
}
|
||||
foreach (IStartup startup in Startups)
|
||||
{
|
||||
startup.Start((app as IHost).Services);
|
||||
RunDebugStep("Startup", $"执行启动回调 {startup.GetType().FullName}", () => startup.Start((app as IHost).Services));
|
||||
}
|
||||
|
||||
foreach (IModule module in Modules)
|
||||
{
|
||||
module.UseModule(app);
|
||||
RunDebugStep("Module", $"装配管道 {module.GetType().FullName}", () => module.UseModule(app));
|
||||
}
|
||||
Debug.WriteLine("[Store] 应用管道装配完成");
|
||||
return app;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
@@ -26,6 +26,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\MAX.Core\MAX.Core.csproj" />
|
||||
<ProjectReference Include="..\MAX.Plugins\MAX.Plugins.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace MAX.Plugins;
|
||||
|
||||
/// <summary>独立插件入口。实现类必须具有无参构造函数。</summary>
|
||||
public interface IPlugin
|
||||
{
|
||||
string Id { get; }
|
||||
int Priority { get; }
|
||||
void ConfigureServices(IServiceCollection services, PluginContext context);
|
||||
void Configure(IApplicationBuilder app, PluginContext context);
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<ItemGroup><FrameworkReference Include="Microsoft.AspNetCore.App" /></ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,32 @@
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace MAX.Plugins;
|
||||
|
||||
public abstract class PluginBase : IPlugin
|
||||
{
|
||||
public abstract string Id { get; }
|
||||
public virtual int Priority => 0;
|
||||
protected PluginContext Context { get; private set; } = null!;
|
||||
|
||||
public void ConfigureServices(IServiceCollection services, PluginContext context)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(services);
|
||||
ArgumentNullException.ThrowIfNull(context);
|
||||
if (!string.Equals(Id, context.Id, StringComparison.Ordinal))
|
||||
throw new ArgumentException("插件上下文标识不匹配。", nameof(context));
|
||||
if (Context is not null) throw new InvalidOperationException($"插件 {Id} 已注册。");
|
||||
Context = context;
|
||||
RegisterServices(services);
|
||||
}
|
||||
|
||||
public void Configure(IApplicationBuilder app, PluginContext context)
|
||||
{
|
||||
if (!ReferenceEquals(Context, context))
|
||||
throw new InvalidOperationException($"插件 {Id} 尚未注册或上下文不匹配。");
|
||||
ConfigureApplication(app);
|
||||
}
|
||||
protected abstract void RegisterServices(IServiceCollection services);
|
||||
protected virtual void ConfigureApplication(IApplicationBuilder app) { }
|
||||
protected string RequireConfiguration(string key) => Context.RequireConfiguration(key);
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
||||
namespace MAX.Plugins;
|
||||
|
||||
/// <summary>插件独立配置,不合并宿主或其他插件配置。</summary>
|
||||
public sealed class PluginContext : IDisposable
|
||||
{
|
||||
private int disposed;
|
||||
public string Id { get; }
|
||||
public string DirectoryPath { get; }
|
||||
public IHostEnvironment Environment { get; }
|
||||
public IConfigurationRoot Configuration { get; }
|
||||
|
||||
public PluginContext(string id, string directoryPath, IHostEnvironment environment)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(id);
|
||||
ArgumentNullException.ThrowIfNull(environment);
|
||||
Id = id;
|
||||
DirectoryPath = Path.GetFullPath(directoryPath);
|
||||
Environment = environment;
|
||||
Configuration = new ConfigurationBuilder()
|
||||
.SetBasePath(DirectoryPath)
|
||||
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: false)
|
||||
.AddJsonFile($"appsettings.{environment.EnvironmentName}.json", optional: true, reloadOnChange: false)
|
||||
.AddEnvironmentVariables($"Plugins__{id}__")
|
||||
.Build();
|
||||
}
|
||||
|
||||
public string RequireConfiguration(string key) =>
|
||||
!string.IsNullOrWhiteSpace(Configuration[key]) ? Configuration[key]!
|
||||
: throw new InvalidOperationException($"插件 {Id} 缺少配置 {key},请检查 {Path.Combine(DirectoryPath, "appsettings.json")}。");
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (Interlocked.Exchange(ref disposed, 1) == 0)
|
||||
(Configuration as IDisposable)?.Dispose();
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user