Reputation: 9870
I have a service with a plugin architecture that requests plugins from a web api and extracts them into their own directory (Plugins/plugin_id). Each plugin is loaded into a custom PluginLoadContext, which is an inherited AssemblyLoadContext with an override Load method. The Load method allows me to share assemblies between the main service and the plugins:
// 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;
}
}
In addition to their own ALC each plugin also gets a DI container to expose configurability to the plugin writers. DI Containers use Autofac. Each Autofac.ContainerBuilder is loaded with the relevant types from the current plugin as well as additional dependency registrations
// 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");
}
}
I'm trying to allow my plugin to read the main service's config file and bind the data into Settings objects for instance:
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;
}
}
}
And the config looks like:
"ServiceSettings": {
"ServiceURL": "https://localhost:7025",
"PayloadChunkSize": 500000,
"InstallerWorkingFolder": "C:\\my_install_tmp\\",
"PluginDirectory": "Plugins"
}
However, after instancing ServiceSettings, ServiceURL is still null. This is not the only place I'm reading this config file. The main service does the same thing, using the same ServiceSettings class. The only difference is that I'm loading it using Microsoft DI rather than 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();
When I set breakpoints, I don't get into the ServiceSettings constructor twice, but I do get into the constructor of the dependent class twice. This would be the ClientService, which in turn is depended on by RequestProcessorRole. The role is constructed by the main service out of its own plugin's DI container:
// 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
}
So my question is WHY is my ServiceSettings.ServiceURL empty? I wish this question was easier to ask, but with all the DI and ALCs I could be stepping on myself and not quite realize it
Upvotes: 1
Views: 177