I'd like one of my RESTful services to support plugins. Currently it uses references hardwired at compile-time.
The Application Parts seem to be THE solution so I gave it a try and created a small proof-of-concept service.
It loads plugins located in the ext
subdirectory. Each plugin is located in its own subdirectory named after the plugin itself and it may contain its own dependencies e.g.:
ext\PluginTest.HalloWord\PluginTest.HalloWorld.dll
ext\PluginTest.HalloWord\PluginTest.HalloWorldHelper.dll
I implemented it by loading plugins with the ConfigureApplicationPartManager
and later if plugin dependencies are requested I try to resolve them in the plugin's directory with the AssemblyResolve
event handler. Additionaly each assembly can also contain embedded views. The Razor view engine is able to find them via EmbeddedFileProvider
for each loaded plugin.
public class Startup
{
private const string PluginsDirectoryName = "ext";
public Startup(IConfiguration configuration, IHostingEnvironment hostingEnvironment)
{
Configuration = configuration;
HostingEnvironment = hostingEnvironment;
}
public IConfiguration Configuration { get; }
public IHostingEnvironment HostingEnvironment { get; }
public void ConfigureServices(IServiceCollection services)
{
AppDomain.CurrentDomain.AssemblyResolve += (sender, e) =>
{
var pluginName = e.RequestingAssembly.GetName().Name;
// Extract dependency name from the full assembly name:
// PluginTest.HalloWorldHelper, Version = 1.0.0.0, Culture = neutral, PublicKeyToken = null
var pluginDependencyName = e.Name.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).First();
var pluginDependencyFullName =
Path.Combine(
HostingEnvironment.ContentRootPath,
PluginsDirectoryName,
pluginName,
$"{pluginDependencyName}.dll"
);
return
File.Exists(pluginDependencyFullName)
? Assembly.LoadFile(pluginDependencyFullName)
: null;
};
var pluginAssemblies =
GetPluginAssemblies(HostingEnvironment)
.ToList();
services
.AddMvc()
.ConfigureApplicationPartManager(apm =>
{
foreach (var pluginAssembly in pluginAssemblies)
{
apm.ApplicationParts.Add(new AssemblyPart(pluginAssembly));
}
});
// Views are embeded in plugins so add a resolver so that the Razor view engine can find them.
services.Configure<RazorViewEngineOptions>(options =>
{
foreach (var pluginAssembly in pluginAssemblies)
{
options
.FileProviders
.Add(new EmbeddedFileProvider(pluginAssembly));
}
});
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseMvc();
}
private static IEnumerable<Assembly> GetPluginAssemblies(IHostingEnvironment hostingEnvironment)
{
var pluginDirectoryName = Path.Combine(hostingEnvironment.ContentRootPath, PluginsDirectoryName);
if (!Directory.Exists(pluginDirectoryName))
{
yield break;
}
var pluginDirectories = Directory.GetDirectories(pluginDirectoryName);
foreach (var pluginDirectory in pluginDirectories)
{
var pluginFullName =
Path.Combine(
hostingEnvironment.ContentRootPath,
pluginDirectory,
$"{Path.GetFileName(pluginDirectory)}.dll"
);
if (File.Exists(pluginFullName))
{
yield return Assembly.LoadFile(pluginFullName);
}
}
}
}
My PoC works, loads plugins, their dependencies and correctly executes controllers that were loaded dynamically.
I was wondering whether there is anything about this simple solution that could be done better?
_hostingEnvironment
is coming from, but assuming that was a typo while copying over the code? \$\endgroup\$AddMvc
callAddOptions
internally? Not seeing where that is enabled as well. \$\endgroup\$AddOptions
- I don't think I'm using this one... or do you mean thisservices.Configure<RazorViewEngineOptions>(options =>
? I didn't have to activate anything else for this to work. It's the exact code I'm using. NoAddOptions
anywhere. \$\endgroup\$