提问人:Wes P 提问时间:9/29/2022 更新时间:9/29/2022 访问量:120
无法读取依赖注入插件中的配置文件
Cannot read config file in dependency injected plugin
问:
我有一个带有插件架构的服务,它从 Web API 请求插件并将它们提取到自己的目录 (Plugins/plugin_id)。每个插件都加载到自定义 PluginLoadContext 中,该自定义 PluginLoadContext 是带有重写 Load 方法的继承 AssemblyLoadContext。Load 方法允许我在主服务和插件之间共享程序集:
// PluginLoadContext.cs
private List<string> _SharedAssemblyNames = new List<string> {
"MyProject.PluginBase",
"MyProject.ServiceClient",
"System.Runtime",
"Microsoft.Extensions.Logging.Abstractions",
"Microsoft.Extensions.Configuration",
"Microsoft.Extensions.Configuration.Abstractions",
"Microsoft.Extensions.Configuration.FileExtensions",
"Microsoft.Extensions.Configuration.Json",
"Microsoft.Extensions.Hosting.Abstractions",
"MyProject.Common",
"Autofac",
"Autofac.Configuration"
};
protected override Assembly? Load(AssemblyName assemblyName)
{
if (!_IsConfigured) throw new Exception("Must call method Configure on a new PluginLoadContext");
if (assemblyName == null || String.IsNullOrWhiteSpace(assemblyName.Name)) return null;
if (_SharedAssemblyNames.Contains(assemblyName.Name))
{
_Logger.LogDebug($"Loading '{assemblyName}' from SERVICE");
if (!_SharedAssemblies.ContainsKey(assemblyName.Name))
{
var context = GetLoadContext(Assembly.GetExecutingAssembly());
if(context == null) return null;
var assm = context.Assemblies.FirstOrDefault(a => a.GetName().Name == assemblyName.Name);
if (assm == null) return null;
_SharedAssemblies.Add(assemblyName.Name, assm);
}
return _SharedAssemblies[assemblyName.Name];
}
else
{
// _Resolver is a AssemblyDependencyResolver pointing to the plugin directory
var assemblyPath = _Resolver!.ResolveAssemblyToPath(assemblyName);
if(assemblyPath != null)
{
_Logger.LogDebug($"Loading '{assemblyName}' from PLUGIN");
return LoadFromAssemblyPath(assemblyPath);
}
_Logger.LogWarning($"Could not find assembly '{assemblyName}' to load");
return null;
}
}
除了自己的 ALC 之外,每个插件还获得了一个 DI 容器,以向插件编写者公开可配置性。DI 容器使用 Autofac。每个 Autofac.ContainerBuilder 都加载了当前插件中的相关类型以及其他依赖项注册
// Plugin's Setup.cs is detected and executed dynamically after loading the plugin assembly(ies) into it's ALC
public class Setup
{
public Plugin Configure(PluginBuilder builder)
{
return builder
// Custom configuration which registers a particular type to perform a job within the loaded plugin
.ConfigureRole<RequestProcessorRole>()
// Adds configurable dependencies
.ConfigureDependencies(builder =>
{
// Foreshadowing... my problem, which I haven't described yet, is with reading this config file.
var config = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: false);
var module = new ConfigurationModule(config.Build());
builder.RegisterModule(module);
// This guy is contained within MyProject.ServiceClient
builder.RegisterType<ClientService>().As<IClientService>();
// This guy is local to the plugin
builder.RegisterType<RequestHandlerService>().InstancePerDependency();
// These classes read from the config file
// These guys are contained in MyProject.Common
builder.RegisterType<SecuritySettings>().As<ISecuritySettings>().SingleInstance();
builder.RegisterType<ServiceSettings>().As<IServiceSettings>().SingleInstance();
})
// Finalizes and constructs the plugin metadata and DI container.
// I can share this, but there really isn't a whole lot of interesting stuff going on here
.BuildPlugin("RequestProcessor");
}
}
我正在尝试允许我的插件读取主服务的配置文件并将数据绑定到 Settings 对象中,例如:
public sealed class ServiceSettings : IServiceSettings
{
public const string KEY = "ServiceSettings";
private const int _PAYLOAD_CHUNK_DEFAULT = 1000000; // ~1MB
public string ServiceURL { get; private set; } = String.Empty;
public int PayloadChunkSize { get; private set; } = _PAYLOAD_CHUNK_DEFAULT;
public string InstallerWorkingFolder { get; private set; } = @"C:\install_tmp";
public int DefaultTaskDelay_Slow { get; private set; } = 5000;
public int DefaultTaskDelay_Fast { get; private set; } = 100;
public string PluginDirectory { get; private set; } = "Plugins";
public ServiceSettings() { }
public ServiceSettings(ILogger<ServiceSettings> logger, IConfiguration configuration)
{
logger.LogInformation("Loading Service configuration...");
var section = configuration.GetSection(KEY).Get<ServiceSettings>(o => { o.BindNonPublicProperties = true; });
if (section == null)
{
throw new Exception("Loading configuration for 'Service' failed.");
}
ServiceURL = section.ServiceURL;
PayloadChunkSize = section.PayloadChunkSize;
InstallerWorkingFolder = section.InstallerWorkingFolder;
if (PayloadChunkSize == 0)
{
PayloadChunkSize = _PAYLOAD_CHUNK_DEFAULT;
}
}
}
配置如下所示:
"ServiceSettings": {
"ServiceURL": "https://localhost:7025",
"PayloadChunkSize": 500000,
"InstallerWorkingFolder": "C:\\my_install_tmp\\",
"PluginDirectory": "Plugins"
}
但是,实例化 ServiceSettings 后,ServiceURL 仍为 null。这不是我阅读此配置文件的唯一地方。主服务使用相同的 ServiceSettings 类执行相同的操作。唯一的区别是我使用 Microsoft DI 而不是 Autofac 加载它:
using IHost host =
Host.CreateDefaultBuilder(args)
//... other config
.ConfigureAppConfiguration(config => {
config.AddJsonFile("appsettings.json", optional: false, reloadOnChange: false);
})
.ConfigureServices(services =>
{
services.AddSingleton<IServiceSettings, ServiceSettings>();
services.AddSingleton<IClientService, ClientService>();
...
})
//... other config
.Build();
await host.RunAsync();
当我设置断点时,我不会两次进入 ServiceSettings 构造函数,但我会两次进入依赖类的构造函数。这将是 ClientService,而 RequestProcessorRole 又依赖于它。该角色由主服务从其自己的插件的 DI 容器中构造出来:
// Role instance construction in my PluginManager class
public RoleBase? CreateRoleInstance(Plugin plugin, Type t)
{
var instance = plugin.Container.Resolve(t) as RoleBase;
return instance;
}
// RequestProcessorRole constructor, requiring the ClientService
public RequestProcessorRole(IClientService service, ILogger<RequestProcessorRole> logger)
{
_Service = service;
_Logger = logger;
}
// The ClientService requiring the ServiceSettings
public sealed class ClientService : IClientService
{
// ... other stuff
private IServiceSettings _ServiceSettings;
public ClientService(ILogger<IClientService> logger, IServiceSettings serviceSettings)
{
_ServiceSettings = serviceSettings;
}
// ... other stuff
}
所以我的问题是为什么我的ServiceSettings.ServiceURL为空?我希望这个问题更容易问,但是对于所有的 DI 和 ALC,我可能会踩到自己而没有完全意识到这一点
答: 暂无答案
评论