Convert Figma logo to code with AI

natemcmaster logoDotNetCorePlugins

.NET Core library for dynamically loading code

1,710
234
1,710
10

Top Related Projects

This repository contains a suite of libraries that provide facilities commonly needed when creating production-ready applications.

2,849

Cecil is a library to inspect, modify and create .NET programs and libraries.

4,457

Extensible tool for weaving .net assemblies

2,267

Castle Core, including Castle DynamicProxy, Logging Services and DictionaryAdapter

4,580

An addictive .NET IoC container

Quick Overview

DotNetCorePlugins is a library for .NET Core that enables dynamic loading of assemblies at runtime. It provides a simple API for loading and executing code from external assemblies, allowing developers to create plugin-based architectures in their .NET Core applications.

Pros

  • Enables modular application design through plugin architecture
  • Supports hot-reloading of plugins without restarting the application
  • Compatible with .NET Core 2.1+ and .NET 5+
  • Provides a simple and intuitive API for loading and managing plugins

Cons

  • May introduce security risks if not properly implemented
  • Can increase application complexity
  • Limited documentation for advanced scenarios
  • Potential performance overhead due to dynamic loading

Code Examples

Loading a plugin:

var loader = PluginLoader.CreateFromAssemblyFile(
    assemblyFile: "./plugins/MyPlugin.dll",
    sharedTypes: new[] { typeof(IPlugin) });

var assembly = loader.LoadDefaultAssembly();
var pluginType = assembly.GetTypes().First(t => typeof(IPlugin).IsAssignableFrom(t));
var plugin = (IPlugin)Activator.CreateInstance(pluginType);
plugin.DoSomething();

Hot-reloading a plugin:

var loader = PluginLoader.CreateFromAssemblyFile(
    assemblyFile: "./plugins/MyPlugin.dll",
    sharedTypes: new[] { typeof(IPlugin) },
    isUnloadable: true);

// Initial load
var assembly = loader.LoadDefaultAssembly();
var plugin = CreatePlugin(assembly);
plugin.DoSomething();

// Reload
loader.Reload();
assembly = loader.LoadDefaultAssembly();
plugin = CreatePlugin(assembly);
plugin.DoSomething();

Using dependency injection with plugins:

var services = new ServiceCollection()
    .AddSingleton<IPluginManager, PluginManager>()
    .AddSingleton(sp => PluginLoader.CreateFromAssemblyFile(
        assemblyFile: "./plugins/MyPlugin.dll",
        sharedTypes: new[] { typeof(IPlugin) }))
    .BuildServiceProvider();

var pluginManager = services.GetRequiredService<IPluginManager>();
var plugin = pluginManager.GetPlugin<IPlugin>();
plugin.DoSomething();

Getting Started

  1. Install the NuGet package:

    dotnet add package McMaster.NETCore.Plugins
    
  2. Create a plugin interface:

    public interface IPlugin
    {
        void DoSomething();
    }
    
  3. Implement the plugin in a separate project:

    public class MyPlugin : IPlugin
    {
        public void DoSomething()
        {
            Console.WriteLine("Plugin is doing something!");
        }
    }
    
  4. Load and use the plugin in your main application:

    var loader = PluginLoader.CreateFromAssemblyFile(
        assemblyFile: "./plugins/MyPlugin.dll",
        sharedTypes: new[] { typeof(IPlugin) });
    
    var assembly = loader.LoadDefaultAssembly();
    var pluginType = assembly.GetTypes().First(t => typeof(IPlugin).IsAssignableFrom(t));
    var plugin = (IPlugin)Activator.CreateInstance(pluginType);
    plugin.DoSomething();
    

Competitor Comparisons

This repository contains a suite of libraries that provide facilities commonly needed when creating production-ready applications.

Pros of Extensions

  • Broader scope, offering a wide range of extensions for various .NET functionalities
  • Official Microsoft repository, ensuring long-term support and integration with .NET ecosystem
  • More frequent updates and active community involvement

Cons of Extensions

  • Less focused on plugin-specific functionality
  • May require more setup and configuration for plugin-like features
  • Potentially steeper learning curve due to broader scope

Code Comparison

DotNetCorePlugins:

var loader = PluginLoader.CreateFromAssemblyFile(
    assemblyFile: "./plugins/MyPlugin.dll",
    sharedTypes: new[] { typeof(IPlugin) });
var plugin = loader.LoadDefaultAssembly()
    .CreateInstance("MyPlugin.PluginClass") as IPlugin;

Extensions:

services.AddOptions<MyOptions>()
    .Configure<IConfiguration>((options, configuration) =>
    {
        configuration.GetSection("MyOptions").Bind(options);
    });

Summary

While DotNetCorePlugins focuses specifically on plugin functionality, Extensions offers a broader range of .NET extensions. DotNetCorePlugins may be more suitable for projects requiring straightforward plugin implementation, while Extensions provides a comprehensive set of tools for various .NET development needs. The choice between the two depends on the specific requirements of your project and the desired level of integration with the .NET ecosystem.

2,849

Cecil is a library to inspect, modify and create .NET programs and libraries.

Pros of Cecil

  • More mature and widely used library for .NET assembly manipulation
  • Offers deeper low-level control over IL code and metadata
  • Supports a broader range of .NET assembly operations (reading, writing, modifying)

Cons of Cecil

  • Steeper learning curve due to its low-level nature
  • Requires more manual work for plugin-related tasks
  • Not specifically designed for plugin systems, unlike DotNetCorePlugins

Code Comparison

Cecil (assembly manipulation):

var assembly = AssemblyDefinition.ReadAssembly("MyAssembly.dll");
var type = assembly.MainModule.GetType("MyNamespace.MyClass");
var method = type.Methods.First(m => m.Name == "MyMethod");
// Modify method body...
assembly.Write("ModifiedAssembly.dll");

DotNetCorePlugins (plugin loading):

var loader = PluginLoader.CreateFromAssemblyFile(
    assemblyFile: "MyPlugin.dll",
    sharedTypes: new[] { typeof(IPlugin) });
var assembly = loader.LoadDefaultAssembly();
var plugin = assembly.CreateInstance("MyPlugin.PluginImpl") as IPlugin;
plugin.Execute();

Summary

Cecil is a powerful library for low-level .NET assembly manipulation, offering extensive control over IL code and metadata. DotNetCorePlugins, on the other hand, is specifically designed for creating plugin systems in .NET Core applications, providing a higher-level abstraction for loading and managing plugins. Choose Cecil for advanced assembly manipulation tasks, and DotNetCorePlugins for simpler plugin implementation in .NET Core projects.

4,457

Extensible tool for weaving .net assemblies

Pros of Fody

  • Extensive ecosystem with many plugins for various tasks
  • Modifies IL code at compile-time, potentially offering better performance
  • Supports a wide range of .NET platforms and frameworks

Cons of Fody

  • More complex setup and configuration process
  • Requires careful management of dependencies and versioning
  • May introduce unexpected behavior due to IL manipulation

Code Comparison

Fody (using PropertyChanged.Fody):

public class Person : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;
    public string Name { get; set; }
}

DotNetCorePlugins:

public class PluginLoadContext : AssemblyLoadContext
{
    private AssemblyDependencyResolver _resolver;
    public PluginLoadContext(string pluginPath)
    {
        _resolver = new AssemblyDependencyResolver(pluginPath);
    }
}

Fody modifies the IL code to automatically implement INotifyPropertyChanged, while DotNetCorePlugins focuses on loading assemblies dynamically at runtime. The code examples highlight their different approaches: Fody simplifies property change notifications, whereas DotNetCorePlugins manages assembly loading for plugin systems.

2,267

Castle Core, including Castle DynamicProxy, Logging Services and DictionaryAdapter

Pros of Core

  • More comprehensive framework with a wider range of features
  • Longer history and larger community support
  • Offers advanced dependency injection and inversion of control capabilities

Cons of Core

  • Steeper learning curve due to its extensive feature set
  • Potentially heavier and more complex for simple plugin scenarios
  • May introduce unnecessary overhead for projects that only need basic plugin functionality

Code Comparison

Core:

container.Register(Component.For<IPlugin>().ImplementedBy<MyPlugin>());
var plugin = container.Resolve<IPlugin>();
plugin.Execute();

DotNetCorePlugins:

var loader = PluginLoader.CreateFromAssemblyFile("MyPlugin.dll");
var plugin = loader.LoadDefaultAssembly().CreateInstance<IPlugin>();
plugin.Execute();

Summary

Core is a more comprehensive framework with a wider range of features and stronger community support, making it suitable for complex applications. However, it may be overkill for simple plugin scenarios. DotNetCorePlugins offers a more lightweight and focused approach to plugin management, which can be advantageous for projects with specific plugin needs. The choice between the two depends on the project's requirements and complexity.

4,580

An addictive .NET IoC container

Pros of Autofac

  • More mature and widely adopted in the .NET ecosystem
  • Supports a broader range of dependency injection scenarios
  • Offers advanced features like circular dependency resolution and property injection

Cons of Autofac

  • Steeper learning curve due to its extensive feature set
  • Can be overkill for simpler applications or microservices
  • Slightly higher performance overhead compared to lighter DI containers

Code Comparison

Autofac:

var builder = new ContainerBuilder();
builder.RegisterType<MyService>().As<IService>();
var container = builder.Build();
var service = container.Resolve<IService>();

DotNetCorePlugins:

var loader = PluginLoader.CreateFromAssemblyFile(
    assemblyFile,
    sharedTypes: new[] { typeof(IPlugin) });
var assembly = loader.LoadDefaultAssembly();
var plugin = assembly.CreateInstance("MyPlugin") as IPlugin;

While Autofac focuses on dependency injection and inversion of control, DotNetCorePlugins specializes in loading assemblies at runtime for plugin-based architectures. Autofac provides a more comprehensive solution for managing object lifecycles and dependencies, whereas DotNetCorePlugins offers a lightweight approach to dynamically loading and unloading assemblies in .NET Core applications.

Convert Figma logo designs to code with AI

Visual Copilot

Introducing Visual Copilot: A new AI model to turn Figma designs to high quality code using your components.

Try Visual Copilot

README

.NET Plugins

Build Status Code Coverage NuGet NuGet Downloads

This project provides API for loading .NET assemblies dynamically, executing them as extensions to the main application, and finding and isolating the dependencies of the plugin from the main application. It allows fine-grained control over assembly isolation and type sharing. Read more details about type sharing below.

Note

2.0+ of library supports .NET 8. If you still need .NET < 8 support, look for an old 1.* version of this library.

Blog post introducing this project, July 25, 2018: .NET Core Plugins: Introducing an API for loading .dll files (and their dependencies) as 'plugins'.

Since 2018, .NET Core 3 was released, and it added stdlib API to improve assembly loading. If you are interested in understanding that API, see "Create a .NET Core application with plugins" on docs.microsoft.com. The result of this tutorial would be a simpler version of DotNetCorePlugins, but missing some features like an API for unifying types across the load context boundary, hot reload, and .NET Core 2 support.

Getting started

Install the McMaster.NETCore.Plugins NuGet package.

dotnet add package McMaster.NETCore.Plugins

The main API to use is PluginLoader.CreateFromAssemblyFile.

PluginLoader.CreateFromAssemblyFile(
    assemblyFile: "./plugins/MyPlugin/MyPlugin1.dll",
    sharedTypes: new [] { typeof(IPlugin), typeof(IServiceCollection), typeof(ILogger) },
    isUnloadable: true)
  • assemblyFile = the file path to the main .dll of the plugin
  • sharedTypes = a list of types which the loader should ensure are unified. (See What is a shared type?)
  • isUnloadable = Allow this plugin to be unloaded from memory at some point in the future. (Requires ensuring that you have cleaned up all usages of types from the plugin before unloading actually happens.)

See example projects in samples/ for more detailed, example usage.

Usage

Using plugins requires at least two projects: (1) the 'host' app which loads plugins and (2) the plugin, but typically also uses a third, (3) an contracts project which defines the interaction between the plugin and the host.

For a fully functional sample of this, see samples/hello-world/ .

The plugin contract

You can define your own plugin contract. A minimal contract might look like this.

public interface IPlugin
{
    string GetName();
}

There is nothing special about the name "IPlugin" or the fact that it's an interface. This is just here to illustrate a concept. Look at samples/ for additional examples of ways you could define the interaction between host and plugins.

The plugins

Warning

Using netstandard2.0 as the TargetFramework for your plugin project has known issues. Use net8.0 instead.

A minimal implementation of the plugin could be as simple as this.

internal class MyPlugin1 : IPlugin
{
    public string GetName() => "My plugin v1";
}

As mentioned above, this is just an example. This library doesn't require the use of "IPlugin" or interfaces or "GetName()" methods. This code is only here to demonstrates how you can decouple hosts and plugins, but still use interfaces for type-safe interactions.

The host

The host application can load plugins using the PluginLoader API. The host app needs to define a way to find the assemblies for the plugin on disk. One way to do this is to follow a convention, such as:

plugins/
    $PluginName1/
        $PluginName1.dll
        (additional plugin files)
    $PluginName2/
        $PluginName2.dll

It is important that each plugin is published into a separate directory. This will avoid contention between plugins and duplicate dependency issues.

You can prepare the sample plugin above by running

dotnet publish MyPlugin1.csproj --output plugins/MyPlugin1/

An implementation of a host which finds and loads this plugin might look like this. This sample uses reflection to find all types in plugins which implement IPlugin, and then initializes the types' parameter-less constructors. This is just one way to implement a host. More examples of how to use plugins can be found in samples/.

using McMaster.NETCore.Plugins;

var loaders = new List<PluginLoader>();

// create plugin loaders
var pluginsDir = Path.Combine(AppContext.BaseDirectory, "plugins");
foreach (var dir in Directory.GetDirectories(pluginsDir))
{
    var dirName = Path.GetFileName(dir);
    var pluginDll = Path.Combine(dir, dirName + ".dll");
    if (File.Exists(pluginDll))
    {
        var loader = PluginLoader.CreateFromAssemblyFile(
            pluginDll,
            sharedTypes: new [] { typeof(IPlugin) });
        loaders.Add(loader);
    }
}

// Create an instance of plugin types
foreach (var loader in loaders)
{
    foreach (var pluginType in loader
        .LoadDefaultAssembly()
        .GetTypes()
        .Where(t => typeof(IPlugin).IsAssignableFrom(t) && !t.IsAbstract))
    {
        // This assumes the implementation of IPlugin has a parameterless constructor
        IPlugin plugin = (IPlugin)Activator.CreateInstance(pluginType);

        Console.WriteLine($"Created plugin instance '{plugin.GetName()}'.");
    }
}

What is a shared type?

By default, each instance of PluginLoader represents a unique collection of assemblies loaded into memory. This can make it difficult to use the plugin if you want to pass information from plugin to the host and vice versa. Shared types allow you define the kinds of objects that will be passed between plugin and host.

For example, let's say you have a simple host app like samples/hello-world/, and two plugins which were compiled with a reference interface IPlugin. This interface comes from Contracts.dll. When the application runs, by default, each plugin and the host will have their own version of Contracts.dll which .NET will keep isolated.

The problem with this isolation is that an object of IPlugin created within the "PluginApple" or "PluginBanana" context does not appear to be an instance of IPlugin in any of the other plugin contexts.

DefaultConfigDiagram

Configuring a shared type of IPlugin allows the .NET to pass objects of this type across the plugin isolation boundary. It does this by ignoring the version of Contracts.dll in each plugin folder, and sharing the version that comes with the Host.

SharedTypes

Read even more details about shared types here.

Support for MVC and Razor

A common usage for plugins is to load class libraries that contain MVC controllers or Razor Pages. You can set up an ASP.NET Core to load controllers and views from a plugin using the McMaster.NETCore.Plugins.Mvc package.

dotnet add package McMaster.NETCore.Plugins.Mvc

The main API to use is .AddPluginFromAssemblyFile(), which can be chained onto the call to .AddMvc() or .AddRazorPages() in the Startup.ConfigureServices method.

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        var pluginFile = Path.Combine(AppContext.BaseDirectory, "plugins/MyRazorPlugin/MyRazorPlugin.dll");
        services
            .AddMvc()
            // The AddPluginFromAssemblyFile method comes from McMaster.NETCore.Plugins.Mvc
            .AddPluginFromAssemblyFile(pluginFile);
    }
}

See example projects in samples/aspnetcore-mvc/ for more detailed, example usage.

Reflection

Sometimes you may want to use a plugin along with reflection APIs such as Type.GetType(string typeName) or Assembly.Load(string assemblyString). Depending on where these APIs are used, they might fail to load the assemblies in your plugin. There is an API which you can use to set the ambient context which .NET's reflection APIs will use to load the correct assemblies from your plugin.

Example:

var loader = PluginLoader.CreateFromAssemblyFile("./plugins/MyPlugin/MyPlugin1.dll");

using (loader.EnterContextualReflection())
{
    var myPluginType = Type.GetType("MyPlugin.PluginClass");
    var myPluginAssembly = Assembly.Load("MyPlugin1");
}

Read this post written by .NET Core engineers for even more details on contextual reflection.

Overriding the Default Load Context

Under the hood, DotNetCorePlugins is using a .NET API called ApplicationLoadContext. This creates a scope for resolving assemblies. By default, PluginLoader will create a new context and fallback to a default context if it cannot find an assembly or if type sharing is enabled. The default fallback context is inferred when PluginLoader is instantiated. In certain advanced scenarios, you may need to manually change the default context, for instance, plugins which then load more plugins, or when running .NET in a custom native host.

To override the default assembly load context, set PluginConfig.DefaultContext. Example:

AssemblyLoadContext myCustomDefaultContext = // (something).
PluginLoader.CreateFromAssemblyFile(dllPath,
     config => config.DefaultContext = myCustomDefaultContext);