无法读取依赖注入插件中的配置文件

Cannot read config file in dependency injected plugin

提问人:Wes P 提问时间:9/29/2022 更新时间:9/29/2022 访问量:120

问:

我有一个带有插件架构的服务,它从 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,我可能会踩到自己而没有完全意识到这一点

C# 依赖注入 插件 Autofac 汇编加载

评论


答: 暂无答案