Adding Assemblies/Types to be made available to Razor Page at Runtime - c#

I'm trying to build a dynamic Web interface where I can dynamically point at a folder and serve Web content out of that folder with ASP.NET Core. This works fairly easily by using FileProviders in ASP.NET Core to re-route the Web root folder. This works both for StaticFiles and For RazorPages.
However, for RazorPages the problem is that once you do this you can't dynamically add references for additional types. I'd like to be able to optionally add a folder (PrivateBin) which on startup I can loop through, load the assemblies and then have those assemblies visible in Razor.
Unfortunately it doesn't work as Razor does not appear to see the loaded assemblies even when using runtime compilation.
I use the following during startup to load assemblies. Note the folder that these are loaded from are not in the default ContentRoot or WebRoot but in the new redirected WebRoot.
// WebRoot is a user chosen Path here specified via command line --WebRoot c:\temp\web
private void LoadPrivateBinAssemblies()
{
var binPath = Path.Combine(WebRoot, "PrivateBin");
if (Directory.Exists(binPath))
{
var files = Directory.GetFiles(binPath);
foreach (var file in files)
{
if (!file.EndsWith(".dll", StringComparison.CurrentCultureIgnoreCase) &&
!file.EndsWith(".exe", StringComparison.InvariantCultureIgnoreCase))
continue;
try
{
var asm = AssemblyLoadContext.Default.LoadFromAssemblyPath(file);
Console.WriteLine("Additional Assembly: " + file);
}
catch (Exception ex)
{
Console.WriteLine("Failed to load private assembly: " + file);
}
}
}
}
The assembly loads into the AssemblyLoadContext() and I can - using Reflection and Type.GetType("namespace.class,assembly") - access the type.
However, when I try to access the type in RazorPages - even with Runtime Compilation enabled - the types are not available. I get the following error:
To make sure that the type is indeed available, I checked that I can do the following inside of Razor:
#{
var md = Type.GetType("Westwind.AspNetCore.Markdown.Markdown,Westwind.AspNetCore.Markdown");
var mdText = md.InvokeMember("Parse", BindingFlags.InvokeMethod | BindingFlags.Public | BindingFlags.Static, null,
null, new object[] { "**asdasd**", false, false, false });
}
#mdText
and that works fine. So the assembly is loaded and the type is accessible, but Razor doesn't appear to be aware of it.
So the question is:
Is it possible to load assemblies at runtime and make them available to Razor with Runtime Compilation, and use it like you normally would use a type via direct declarative access?

It turns out the solution to this is via the Razor Runtime Compilation Options which allow adding of extra 'ReferencePaths', and then explicitly loading assemblies.
In ConfigureServices():
services.AddRazorPages(opt => { opt.RootDirectory = "/"; })
.AddRazorRuntimeCompilation(
opt =>
{
opt.FileProviders.Add(new PhysicalFileProvider(WebRoot));
LoadPrivateBinAssemblies(opt);
});
then:
private void LoadPrivateBinAssemblies(MvcRazorRuntimeCompilationOptions opt)
{
var binPath = Path.Combine(WebRoot, "PrivateBin");
if (Directory.Exists(binPath))
{
var files = Directory.GetFiles(binPath);
foreach (var file in files)
{
if (!file.EndsWith(".dll", StringComparison.CurrentCultureIgnoreCase) &&
!file.EndsWith(".exe", StringComparison.InvariantCultureIgnoreCase))
continue;
try
{
var asm = AssemblyLoadContext.Default.LoadFromAssemblyPath(file);
opt.AdditionalReferencePaths.Add(file);
}
catch (Exception ex)
{
...
}
}
}
}
The key is:
opt.AdditionalReferencePaths.Add(file);
which makes the assembly visible to Razor, but doesn't actually load it. To load it you then have to explicitly load it with:
AssemblyLoadContext.Default.LoadFromAssemblyPath(file);
which loads the assembly from a path. Note that any dependencies that this assembly has have to be available either in the application's startup path or in the same folder you're loading from.
Note: Load order for dependencies may be important here or a not previously added assembly may not be found as a dependency (untested).

A Quick look into the ASP.NET Core source code reveals:
All Razor view Compilations start at:
RuntimeViewCompiler.CreateCompilation(..)
which uses:
CSharpCompiler.Create(.., .., references: ..)
which uses:
RazorReferenceManager.CompilationReferences
which uses: see code on github
// simplyfied
var referencePaths = ApplicationPartManager.ApplicationParts
.OfType<ICompilationReferencesProvider>()
.SelectMany(_ => _.GetReferencePaths())
which uses:
ApplicationPartManager.ApplicationParts
So we need somehow register our own ICompilationReferencesProvider and this is how..
ApplicationPartManager
While it's search for Application parts does the ApplicationPartManager a few things:
it searchs for hidden Assemblies reading attributes like:
[assembly: ApplicationPartAttribute(assemblyName:"..")] // Specifies an assembly to be added as an ApplicationPart
[assembly: RelatedAssemblyAttribute(assemblyFileName:"..")] // Specifies a assembly to load as part of MVC's assembly discovery mechanism.
// plus `Assembly.GetEntryAssembly()` gets added automaticly behind the scenes.
Then it loops throuth all found Assemblies and uses ApplicationPartFactory.GetApplicationPartFactory(assembly) (as seen in line 69) to find types which extend ApplicationPartFactory.
Then it invokes the method GetApplicationParts(assembly) on all found ApplicationPartFactorys.
All Assemblies without ApplicationPartFactory get the DefaultApplicationPartFactory which returns new AssemblyPart(assembly) in GetApplicationParts.
public abstract IEnumerable<ApplicationPart> GetApplicationParts(Assembly assembly);
GetApplicationPartFactory
GetApplicationPartFactory searches for [assembly: ProvideApplicationPartFactory(typeof(SomeType))] then it uses SomeType as factory.
public abstract class ApplicationPartFactory {
public abstract IEnumerable<ApplicationPart> GetApplicationParts(Assembly assembly);
public static ApplicationPartFactory GetApplicationPartFactory(Assembly assembly)
{
// ...
var provideAttribute = assembly.GetCustomAttribute<ProvideApplicationPartFactoryAttribute>();
if (provideAttribute == null)
{
return DefaultApplicationPartFactory.Instance; // this registers `assembly` as `new AssemblyPart(assembly)`
}
var type = provideAttribute.GetFactoryType();
// ...
return (ApplicationPartFactory)Activator.CreateInstance(type);
}
}
One Solution
This means we can create and register (using ProvideApplicationPartFactoryAttribute) our own ApplicationPartFactory which returns a custom ApplicationPart implementation which implements ICompilationReferencesProvider and then returns our references in GetReferencePaths.
[assembly: ProvideApplicationPartFactory(typeof(MyApplicationPartFactory))]
namespace WebApplication1 {
public class MyApplicationPartFactory : ApplicationPartFactory {
public override IEnumerable<ApplicationPart> GetApplicationParts(Assembly assembly)
{
yield return new CompilationReferencesProviderAssemblyPart(assembly);
}
}
public class CompilationReferencesProviderAssemblyPart : AssemblyPart, ICompilationReferencesProvider {
private readonly Assembly _assembly;
public CompilationReferencesProviderAssemblyPart(Assembly assembly) : base(assembly)
{
_assembly = assembly;
}
public IEnumerable<string> GetReferencePaths()
{
// your `LoadPrivateBinAssemblies()` method needs to be called before the next line executes!
// So you should load all private bin's before the first RazorPage gets requested.
return AssemblyLoadContext.GetLoadContext(_assembly).Assemblies
.Where(_ => !_.IsDynamic)
.Select(_ => new Uri(_.CodeBase).LocalPath);
}
}
}
My Working Test Setup:
ASP.NET Core 3 WebApplication
ASP.NET Core 3 ClassLibrary
Both Projects have no reference to each other.
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp3.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<Content Remove="Pages\**" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="3.0.0" />
</ItemGroup>
</Project>
services
.AddRazorPages()
.AddRazorRuntimeCompilation();
AssemblyLoadContext.Default.LoadFromAssemblyPath(#"C:\path\to\ClassLibrary1.dll");
// plus the MyApplicationPartFactory and attribute from above.
~/Pages/Index.cshtml
#page
<pre>
output: [
#(
new ClassLibrary1.Class1().Method1()
)
]
</pre>
And it shows the expected output:
output: [
Hallo, World!
]
Have a nice day.

Related

Can't load assembly that isn't explicitly used in my project

I need to load these 4 assemblies on the fly:
"Microsoft.CodeAnalysis",
"Microsoft.CodeAnalysis.CSharp",
"Microsoft.CodeAnalysis.Features",
"Microsoft.CodeAnalysis.CSharp.Features"
They all come from nuget packages referenced in a project separate from the startup project.
But when I try loading them like this:
Assembly.Load("Microsoft.CodeAnalysis"),
Assembly.Load("Microsoft.CodeAnalysis.CSharp"),
Assembly.Load("Microsoft.CodeAnalysis.Features"),
Assembly.Load("Microsoft.CodeAnalysis.CSharp.Features")
// this doesn't work either:
// Assembly.Load("Microsoft.CodeAnalysis.CSharp.Features, Version=3.9.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35")
I get a FileNotFoundException on the Assembly.Load(Microsoft.CodeAnalysis.CSharp.Features) call.
However, loading the dll directly from the packages directory like so:
Assembly.LoadFile(#"D:\project\packages\Microsoft.CodeAnalysis.CSharp.Features.3.9.0-2.20525.2\lib\netstandard2.0\Microsoft.CodeAnalysis.CSharp.Features.dll")
works perfectly fine.
I've also noticed that the Microsoft.CodeAnalysis.CSharp.Features.dll isn't copied to the startup project bin directory, while the three other dlls are. I suspect that it's because I'm not explicitly using that assembly in my own code, it's just loaded using reflection and then immediately sent to external code.
My complete intended usage/implementation looks like this
public static Document CreateDocument(string assemblyName, IEnumerable<PortableExecutableReference> referensMetadata, string documentName = "Script",
IEnumerable<Assembly> hostedAssemblies = null)
{
// To prevent "The language 'C#' is not supported." exception
var _ = typeof(Microsoft.CodeAnalysis.CSharp.Formatting.CSharpFormattingOptions);
var mefHostRequiredAssemblies = new List<Assembly>
{
Assembly.Load("Microsoft.CodeAnalysis"),
Assembly.Load("Microsoft.CodeAnalysis.CSharp"),
Assembly.Load("Microsoft.CodeAnalysis.Features"),
Assembly.Load("Microsoft.CodeAnalysis.CSharp.Features")
};
if (hostedAssemblies != null)
{
mefHostRequiredAssemblies.AddRange(hostedAssemblies);
}
var partTypes = MefHostServices.DefaultAssemblies.Concat(mefHostRequiredAssemblies)
.Distinct()
.SelectMany(x => x.GetTypes())
.ToArray();
var compositionContext = new ContainerConfiguration()
.WithParts(partTypes)
.CreateContainer();
var workspace = new AdhocWorkspace(MefHostServices.Create(compositionContext));
var project = ProjectInfo.Create(ProjectId.CreateNewId(), VersionStamp.Create(),
assemblyName, assemblyName, LanguageNames.CSharp)
.WithMetadataReferences(referensMetadata);
var documentId = DocumentId.CreateNewId(project.Id);
var documentInfo = DocumentInfo.Create(documentId, documentName,
loader: TextLoader.From(
TextAndVersion.Create(
SourceText.From(string.Empty), VersionStamp.Create())
)
);
return workspace.CurrentSolution.AddProject(project)
.AddDocument(documentInfo)
.GetDocument(documentId);
}
My question is what am I doing wrong? How come I can't load references I've explicitly added to the project?

Can Json.Net be embedded into the executable?

I set the 'Embed Interop Types' property of the Netwonsoft.Json library to true and it returns an error:
Cannot embed interop types from assembly
'c:\path\packages\Newtonsoft.Json.9.0.1\lib\net45\Newtonsoft.Json.dll'
because it is missing either the 'ImportedFromTypeLibAttribute' attribute or
the 'PrimaryInteropAssemblyAttribute' attribute
c:\path\packages\Newtonsoft.Json.9.0.1\lib\net45\Newtonsoft.Json.dll
It looks like looking for missing references within the Newtonsoft.Json library, but I am not entirely certain. Is it possible for Json.Net to be embeded into the executable?
You didn't say which language you were using but here is how you'd do it for C#
First, turn off "Embed Interop Types"
Then, to the main executable project, unload and edit the .csproj file, and below the following line:
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
Add this XML to the project file, save, and load it back up.
<Target Name="AfterResolveReferences">
<ItemGroup>
<EmbeddedResource Include="#(ReferenceCopyLocalPaths)" Condition="'%(ReferenceCopyLocalPaths.Extension)' == '.dll'">
<LogicalName>%(ReferenceCopyLocalPaths.DestinationSubDirectory)%(ReferenceCopyLocalPaths.Filename)%(ReferenceCopyLocalPaths.Extension)</LogicalName>
</EmbeddedResource>
</ItemGroup>
</Target>
You’ll then add a new code file to the main project and add the following code to it (modified to fit how your application is named / structured, in a WPF application, a good place to put it would be App.xaml.cs):
[STAThread]
public static void Main()
{
AppDomain.CurrentDomain.AssemblyResolve += OnResolveAssembly;
App.Main(); // Run WPF startup code.
}
private static Assembly OnResolveAssembly(object sender, ResolveEventArgs e)
{
var thisAssembly = Assembly.GetExecutingAssembly();
// Get the Name of the AssemblyFile
var assemblyName = new AssemblyName(e.Name);
var dllName = assemblyName.Name + ".dll";
// Load from Embedded Resources - This function is not called if the Assembly is already
// in the same folder as the app.
var resources = thisAssembly.GetManifestResourceNames().Where(s => s.EndsWith(dllName));
if (resources.Any())
{
// 99% of cases will only have one matching item, but if you don't,
// you will have to change the logic to handle those cases.
var resourceName = resources.First();
using (var stream = thisAssembly.GetManifestResourceStream(resourceName))
{
if (stream == null) return null;
var block = new byte[stream.Length];
// Safely try to load the assembly.
try
{
stream.Read(block, 0, block.Length);
return Assembly.Load(block);
}
catch (IOException)
{
return null;
}
catch(BadImageFormatException)
{
return null;
}
}
}
// in the case the resource doesn't exist, return null.
return null;
}
Finally, make sure you update the target method for your main application to be the main method for the project you just added
Source: http://www.paulrohde.com/merging-a-wpf-application-into-a-single-exe/

How to load assemblies in ASP.NET Core 1.0 RC2

I am migrating my web app from ASP.NET Core RC1 to RC2. I'm trying to load my referenced class libraries.
This code snippet doesn't work with RC2 any more:
public class Startup
{
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
// libraryManager is null ....
ILibraryManager libraryManager = app.GetService<ILibraryManager>();
List<Assembly> result = new List<Assembly>();
IEnumerable<Library> libraries = libraryManager.GetLibraries();
IEnumerable<AssemblyName> assemblyNames = libraries.SelectMany(e => e.Assemblies).Distinct();
assemblyNames = Enumerable.Where(assemblyNames, e => e.Name.StartsWith("projectNamespace"));
foreach (AssemblyName assemblyName in assemblyNames)
{
Assembly assembly = Assembly.Load(assemblyName);
.
.
.
}
}
}
I found a solution. I'm using now DependencyContext instead of ILibraryManager
var loadableAssemblies = new List<Assembly>();
var deps = DependencyContext.Default;
foreach (var compilationLibrary in deps.CompileLibraries)
{
if (compilationLibrary.Name.Contains(projectNamespace))
{
var assembly = Assembly.Load(new AssemblyName(compilationLibrary.Name));
loadableAssemblies.Add(assembly);
}
}
I think stevo has made 2 wrong assumptions:
1) that project namespace should be part of compilation library name.
2) that compilation library name is the same as binary name.
First one is wrong when you change it in project settings.
Second one is wrong when you specify it in buildOptions in project.json.
So your idea is correct but implementation is wrong.
To fix that we need to forget about resolving by namespace until assembly is loaded.
I guess since all assemblies will be loaded in any case we will not get big performance lag.
But it is not panacea... assembly can have multiple root namespaces inside!
So maybe better way will be to define some attribute on assembly level and check it instead of namespace.
In any case if you want to limit your search by assembly name it should be made like this:
IEnumerable<AssemblyName> names = DependencyContext.Default.GetDefaultAssemblyNames();
foreach (AssemblyName name in names)
{
if (name.Name.StartsWith("MyRoot") == true)
{
Assembly assembly = Assembly.Load(name);
// Process assembly here...
// I will check attribute for each loaded assembly found in MyRoot.
}
}

Nancy Module Unit Testing. Exception in unit test

Can't find reason why test isn't work. Has anybody known why it happen? Difference between these two tests is ony in views. In first it is ".html" page and in second it is ".liquid". In my project I use ".liquid", so ".html" is only for testing correct working test.
I have a Nancy Module
public sealed class Module : NancyModule
{
public Module(IBackend storage)
{
Get["/"] = _ => View["Create.liquid"];
Get["/Test"] = _ => View["TestHtml.html"];
}
}
And the tests
[Test]
public void test_html()
{
// Given
var bootstrapper = new ConfigurableBootstrapper(with =>
{
var module = new Module(new Endpoint());
with.Module(module);
});
browser = new Browser(bootstrapper);
// When
var result = browser.Get("/Test", with =>
{
with.HttpRequest();
});
// Then
Assert.AreEqual(HttpStatusCode.OK, result.StatusCode);
}
[Test]
public void test_liquid()
{
// Given
var bootstrapper = new ConfigurableBootstrapper(with =>
{
var module = new Module(new Endpoint());
with.Module(module);
});
browser = new Browser(bootstrapper);
// When
var result = browser.Get("/", with =>
{
with.HttpRequest();
});
// Then
Assert.AreEqual(HttpStatusCode.OK, result.StatusCode);
}
There is exception on second test
System.Exception : ConfigurableBootstrapper Exception
----> Nancy.RequestExecutionException : Oh noes!
----> Nancy.ViewEngines.ViewNotFoundException : Unable to locate view 'Create.liquid'
Currently available view engine extensions: sshtml,html,htm
Locations inspected: ,,,,,,,,views/Module/Create.liquid-en- US,views/Module/Create.liquid,Module/Create.liquid-en-US,Module/Create.liquid,views/Create.liquid-en-US,views/Create.liquid,Create.liquid-en-US,Create.liquid
Root path: D:\Projects\epm-vsp-pasta\Tests\bin\Debug
With Stack Trace
at Nancy.Testing.PassThroughStatusCodeHandler.Handle(HttpStatusCode statusCode, NancyContext context) in d:\Nancy-master\src\Nancy.Testing\PassThroughStatusHandler.cs: line 22
at Nancy.NancyEngine.CheckStatusCodeHandler(NancyContext context) in d:\Nancy-master\src\Nancy\NancyEngine.cs: line 219
at Nancy.NancyEngine.HandleRequest(Request request, Func`2 preRequest) in d:\Nancy-master\src\Nancy\NancyEngine.cs: line 112
at Nancy.NancyEngine.HandleRequest(Request request) in d:\Nancy-master\src\Nancy\NancyEngine.cs: line 77
at Nancy.Testing.Browser.HandleRequest(String method, String path, Action`1 browserContext) in d:\Nancy-master\src\Nancy.Testing\Browser.cs: line 125
at Nancy.Testing.Browser.Get(String path, Action`1 browserContext) in d:\Nancy-master\src\Nancy.Testing\Browser.cs: line 62
at Tests.TestModule.test_liquid() in TestModule.cs: line 111 --RequestExecutionException
at Nancy.NancyEngine.InvokeOnErrorHook(NancyContext context, ErrorPipeline pipeline, Exception ex) in d:\Nancy-master\src\Nancy\NancyEngine.cs: line 272
--ViewNotFoundException
This has to do with .NET assembly loading. Since no types in the Nancy.ViewEngines.Dotliquid assembly is directly used, the .NET compiler thinks it can be smart and not include the reference in the assembly meta data. This results in the assembly not being loaded into the application domain, at all, during run time.
The reason it works for .html files is that the SuperSimpleViewEngine, that manages .html extensions, is built into Nancy.dll and that is loaded.
You can work around this by explicitly using a type from the assembly, for example putting something like var foo = typeof(DotLiquidViewEngine) in your test code, of by using the ViewEngine<DotLiquidViewEngine>() property on the configurable bootstrapper setup.
For our next release, 0.17, we have added code to reduce this effect, as much as possible, by scanning the assemblies in your "bin"-folder and explicitly loading any that references a Nancy* assembly.
Hope this helps
Looks like you haven't referenced the liquid view engine from your test project:
Currently available view engine extensions: sshtml,html,htm
I've seen this occur if the page isn't copied to the output directory.
i.e. check the properties for the .liquid file in Visual Studio and ensure it has the same 'Copy to Output Directory' setting as the working .html file.

Dynamically load DLL files in C# project - how?

In my project I need to use plugins. But to use these in my project I need to import an reference of the plugin. Since I can't know how many or which plugins the project uses beforehand I would like to import them dynamically in my project.
String path = Application.StartupPath;
string[] pluginFiles = Directory.GetFiles(path, "*.dll");
ipi = new IPlugin[pluginFiles.Length];
Assembly asm;
for (int i = 0; i < pluginFiles.Length; i++)
{
string args = pluginFiles[i].Substring(
pluginFiles[i].LastIndexOf("\\") + 1,
pluginFiles[i].IndexOf(".dll") -
pluginFiles[i].LastIndexOf("\\") - 1);
asm = Assembly.LoadFile(pluginFiles[i]);
Type[] types = asm.GetTypes();
In this code example I searched all the .dll files and put them into a string list.
But how can I now load all these .dll files? Or is there a way to use these .dll files without really importing them?
The MEF (Managed Extensibility Framework) Method:
You'll want to add references to System.ComponentModel.Composition to your projects that utilize the import/export functionality of MEF.
First, the bootstrapper/loader (in my case, I just added it to the Main class).
Program.cs:
using System;
using System.Collections.Generic;
using System.ComponentModel.Composition;
using System.ComponentModel.Composition.Hosting;
using MEFContract;
namespace ConsoleApplication5
{
class Program
{
static void Main(string[] args)
{
var prgm = new Program();
// Search the "Plugins" subdirectory for assemblies that match the imports.
var catalog = new DirectoryCatalog("Plugins");
using (var container = new CompositionContainer(catalog))
{
// Match Imports in "prgm" object with corresponding exports in all catalogs in the container
container.ComposeParts(prgm);
}
prgm.DoStuff();
Console.Read();
}
private void DoStuff()
{
foreach (var plugin in Plugins)
plugin.DoPluginStuff();
}
[ImportMany] // This is a signal to the MEF framework to load all matching exported assemblies.
private IEnumerable<IPlugin> Plugins { get; set; }
}
}
The IPlugin interface is the contract between the imports & exports. All plugins will implement this interface. The contract is pretty simple:
IPlugin.cs:
namespace MEFContract
{
public interface IPlugin
{
void DoPluginStuff();
}
}
Finally, you can create as many plugins as you like in different assemblies. They must implement the contract interface and also be decorated with the "Export" attribute to indicate to MEF that they should be matched up with any corresponding imports. Then drop the dlls in a "Plugins" folder (this folder should reside in the same location as the executable). Here's a sample plugin:
Plugin.cs:
using System;
using System.ComponentModel.Composition;
using MEFContract;
namespace Plugin
{
[Export(typeof(IPlugin))]
public class Plugin : IPlugin
{
public void DoPluginStuff()
{
Console.WriteLine("Doing my thing!");
}
}
}
Let's assume for the sake of simplicity that all of the implementations of IPlugin have default constructors (public and no parameters).
That said, you really want to find all types that implement this interface and create an instance of them. You're on the right track somewhat, but you can simplify this tremendously with a little LINQ:
String path = Application.StartupPath;
string[] pluginFiles = Directory.GetFiles(path, "*.dll");
ipi = (
// From each file in the files.
from file in pluginFiles
// Load the assembly.
let asm = Assembly.LoadFile(file)
// For every type in the assembly that is visible outside of
// the assembly.
from type in asm.GetExportedTypes()
// Where the type implements the interface.
where typeof(IPlugin).IsAssignableFrom(type)
// Create the instance.
select (IPlugin) Activator.CreateInstance(type)
// Materialize to an array.
).ToArray();
That said, you might be better off using a dependency injection framework; they usually allow for dynamic loading and binding to interface implementations in assemblies not referenced at compile time.
Also, while a bit convoluted (in my opinion), you might want to look at the System.AddIn namespaces, as they are built specifically for this purpose. However, the dependency injection route is usually much easier if you don't have to worry about version control of contracts and the like.
I have an application which can not only load plugin at runtime, but also hot-load and unload them as user drop them in the folder, take them out or erase them. So, when I need to recompile my plugin, I don't need to re-launch my application. In my case, all plugin derive from the Plugin abstract, so they are easy to find in .DLL.
Here's my loading method:
private static void LoadPlugins(FileInfo file)
{
try
{
Assembly assembly = Assembly.LoadFrom(file.FullName);
foreach (Type type in assembly.GetTypes())
{
if (type.IsSubclassOf(typeof(Plugin)) && type.IsAbstract == false)
{
Plugin b = type.InvokeMember(null,
BindingFlags.CreateInstance,
null, null, null) as Plugin;
plugins.Add(new PluginWrapper(b, file));
b.Register();
}
}
}
catch (ReflectionTypeLoadException ex)
{
StringBuilder sb = new StringBuilder();
foreach (Exception exSub in ex.LoaderExceptions)
{
sb.AppendLine(exSub.Message);
if (exSub is FileNotFoundException)
{
FileNotFoundException exFileNotFound = exSub as FileNotFoundException;
if (!string.IsNullOrEmpty(exFileNotFound.FusionLog))
{
sb.AppendLine("Fusion Log:");
sb.AppendLine(exFileNotFound.FusionLog);
}
}
sb.AppendLine();
}
string errorMessage = sb.ToString();
Log.Error("Plugins Manager", errorMessage);
}
}

Categories