把一些项目移动到插件中
This commit is contained in:
1 parent
d0f9f00b76
commit
5ff52dea92
60 files changed
+410
-27
No files matched your search
@@ -36,7 +36,7 @@ namespace MAX.Framework.DependencyInjection
|
||||
host.AddLogger();
|
||||
//host.Services.AddSingleton<ILoggerFacade>(NLogger.Instance);//注册log
|
||||
|
||||
IEnumerable<Assembly> assemblies = GetApplicationAssemblies();
|
||||
IEnumerable<Assembly> assemblies = StoreAppTypeFinder.Current.GetAssemblies(host.Configuration["Plugins:Path"]).ToArray();
|
||||
// 同一个类可能同时实现 IStartup 与 IModule,这里缓存实例,保证两个阶段拿到的是同一个对象
|
||||
Dictionary<Type, object> instances = [];
|
||||
return AddStoreCore(host, assemblies, instances);
|
||||
@@ -62,7 +62,7 @@ namespace MAX.Framework.DependencyInjection
|
||||
|
||||
|
||||
|
||||
host.Services.AddControllers(options =>
|
||||
var mvc = host.Services.AddControllers(options =>
|
||||
{
|
||||
options.Filters.Add<GlobalExceptionFilter>();
|
||||
options.Filters.Add<UnifyResultFilter>();
|
||||
@@ -87,6 +87,10 @@ namespace MAX.Framework.DependencyInjection
|
||||
};
|
||||
|
||||
});
|
||||
foreach (var assembly in assemblies)
|
||||
{
|
||||
mvc.AddApplicationPart(assembly);
|
||||
}
|
||||
return host.Services;
|
||||
}
|
||||
|
||||
@@ -154,7 +158,7 @@ namespace MAX.Framework.DependencyInjection
|
||||
catch (Exception ex)
|
||||
{
|
||||
// 带上模块名,避免只看到一个没有上下文的异常
|
||||
throw new InvalidOperationException($"模块 {module.GetType().FullName} 的 AddModule 执行失败。", ex);
|
||||
throw new InvalidOperationException($"模块 {module.GetType().FullName} 的 AddModule 执行失败:{ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,132 @@
|
||||
using MAX.Core.Reflections;
|
||||
using MAX.Core.Reflections;
|
||||
using System.Reflection;
|
||||
using System.Runtime.Loader;
|
||||
|
||||
namespace MAX.Framework.DependencyInjection
|
||||
namespace MAX.Framework.DependencyInjection;
|
||||
|
||||
/// <summary>发现宿主程序集及独立发布的插件;入口过滤统一使用 Match。</summary>
|
||||
internal sealed class StoreAppTypeFinder : AppTypeFinder
|
||||
{
|
||||
/// <summary>
|
||||
/// 本应用的类型查找器:在 AppTypeFinder 默认前缀(MAX. / Nexus.)之外,
|
||||
/// 增加 StoreHub、KJD 前缀,用于发现宿主和各业务模块的程序集。
|
||||
/// </summary>
|
||||
internal sealed class StoreAppTypeFinder : AppTypeFinder
|
||||
{
|
||||
public static StoreAppTypeFinder Current { get; } = new();
|
||||
public static StoreAppTypeFinder Current { get; } = new();
|
||||
private readonly Dictionary<string, AssemblyDependencyResolver> resolvers = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly object gate = new();
|
||||
private bool registered;
|
||||
|
||||
protected override List<string> AvailablePrefix()
|
||||
{
|
||||
return ["Store"];
|
||||
}
|
||||
}
|
||||
protected override List<string> AvailablePrefix() => ["Store"];
|
||||
|
||||
public override List<Assembly> GetAssemblies() => GetAssemblies(null);
|
||||
|
||||
public List<Assembly> GetAssemblies(string? pluginPath)
|
||||
{
|
||||
lock (gate)
|
||||
{
|
||||
var path = Path.GetFullPath(string.IsNullOrWhiteSpace(pluginPath) ? "Plugins" : pluginPath, AppContext.BaseDirectory);
|
||||
if (!Directory.Exists(path))
|
||||
return base.GetAssemblies();
|
||||
|
||||
// 每个独立发布目录由入口 .deps.json 标识,避免把共享框架副本当作插件。
|
||||
var entries = Directory.GetFiles(path, "*.deps.json", SearchOption.AllDirectories)
|
||||
.Select(file => file[..^".deps.json".Length] + ".dll")
|
||||
.Where(file => Match(Path.GetFileName(file)))
|
||||
.OrderBy(file => file, StringComparer.OrdinalIgnoreCase).ToArray();
|
||||
foreach (var entry in entries)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!File.Exists(entry))
|
||||
throw new FileNotFoundException("插件入口 DLL 不存在。", entry);
|
||||
resolvers.TryAdd(entry, new AssemblyDependencyResolver(entry));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new InvalidOperationException($"无法初始化插件 {entry}。", ex);
|
||||
}
|
||||
}
|
||||
if (!registered)
|
||||
{
|
||||
AssemblyLoadContext.Default.Resolving += Resolve;
|
||||
AssemblyLoadContext.Default.ResolvingUnmanagedDll += ResolveNative;
|
||||
registered = true;
|
||||
}
|
||||
|
||||
var plugins = new List<Assembly>();
|
||||
foreach (var entry in entries)
|
||||
{
|
||||
try
|
||||
{
|
||||
var assembly = Load(entry);
|
||||
ValidateDependencies(assembly);
|
||||
assembly.GetTypes(); // 插件类型加载失败必须在启动时暴露。
|
||||
plugins.Add(assembly);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var detail = ex is ReflectionTypeLoadException types
|
||||
? string.Join("; ", types.LoaderExceptions.Select(error => error?.Message)) : ex.Message;
|
||||
throw new InvalidOperationException($"加载插件 {entry} 失败:{detail}", ex);
|
||||
}
|
||||
}
|
||||
return base.GetAssemblies().Concat(plugins).DistinctBy(a => a.FullName).ToList();
|
||||
}
|
||||
}
|
||||
|
||||
private static Assembly Load(string path)
|
||||
{
|
||||
var name = AssemblyName.GetAssemblyName(path);
|
||||
var existing = AssemblyLoadContext.Default.Assemblies.FirstOrDefault(a =>
|
||||
string.Equals(a.GetName().Name, name.Name, StringComparison.OrdinalIgnoreCase));
|
||||
if (existing is null)
|
||||
return AssemblyLoadContext.Default.LoadFromAssemblyPath(Path.GetFullPath(path));
|
||||
if (!string.Equals(existing.FullName, name.FullName, StringComparison.OrdinalIgnoreCase))
|
||||
throw new FileLoadException($"程序集版本冲突:请求 {name.FullName},已加载 {existing.FullName}。", path);
|
||||
return existing;
|
||||
}
|
||||
|
||||
private void ValidateDependencies(Assembly assembly)
|
||||
{
|
||||
foreach (var reference in assembly.GetReferencedAssemblies())
|
||||
{
|
||||
// 宿主 .deps.json 优先;插件发布目录中的副本仍须满足相同身份。
|
||||
var candidate = ResolvePath(reference);
|
||||
AssemblyLoadContext.Default.LoadFromAssemblyName(reference);
|
||||
if (candidate is not null)
|
||||
{
|
||||
Load(candidate);
|
||||
// 不强制遍历第三方库所有可选功能依赖;插件类型所需依赖由 GetTypes 校验。
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private string? ResolvePath(AssemblyName name)
|
||||
{
|
||||
string? result = null;
|
||||
foreach (var (entry, resolver) in resolvers)
|
||||
{
|
||||
var candidate = resolver.ResolveAssemblyToPath(name);
|
||||
if (candidate is null) continue;
|
||||
var identity = AssemblyName.GetAssemblyName(candidate);
|
||||
if (result is not null && !string.Equals(AssemblyName.GetAssemblyName(result).FullName, identity.FullName, StringComparison.OrdinalIgnoreCase))
|
||||
throw new FileLoadException($"插件依赖冲突:{result} 与 {candidate}(插件 {entry})。");
|
||||
result ??= candidate;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private Assembly? Resolve(AssemblyLoadContext context, AssemblyName name)
|
||||
{
|
||||
var path = ResolvePath(name);
|
||||
return path is null ? null : Load(path);
|
||||
}
|
||||
|
||||
private IntPtr ResolveNative(Assembly assembly, string name)
|
||||
{
|
||||
// 优先使用请求程序集所属插件目录,避免原生库跨插件误匹配。
|
||||
foreach (var (entry, resolver) in resolvers.OrderByDescending(pair =>
|
||||
assembly.Location.StartsWith(Path.GetDirectoryName(pair.Key)! + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
var path = resolver.ResolveUnmanagedDllToPath(name);
|
||||
if (path is not null) return System.Runtime.InteropServices.NativeLibrary.Load(path);
|
||||
}
|
||||
return IntPtr.Zero;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user