Building extensible applications is a rite of passage for many .NET developers. Whether you’re building a dashboard that needs dynamic widgets or a creative tool that supports third-party effects, you eventually hit the “Plugin Problem.”
In the WPF ecosystem, two architectural patterns have dominated this space: MEF (Managed Extensibility Framework) and Caliburn.Micro’s Parent/Child Bootstrappers.
But regardless of which one you choose, you will eventually face the final boss of modularity: Dependency Conflicts.
In this post, we’ll compare these two approaches and explore how modern .NET features like AssemblyLoadContext allow us to achieve true plugin isolation.
The Contenders#
1. MEF (Managed Extensibility Framework)#
MEF has been the standard for .NET extensibility since .NET Framework 4.0. It relies on a declarative model using attributes.
- Philosophy: “I have a hole here (
[Import]), please fill it with a matching peg ([Export]).” - Pros: Deeply integrated into the ecosystem, supports metadata (lazy loading plugins without instantiating them), and handles complex dependency graphs automatically.
- Cons: Can feel “magical” and hard to debug when composition fails.
| |
2. Caliburn.Micro (Parent/Child Bootstrappers)#
Caliburn.Micro is a powerful MVVM framework. Its approach to modularity is less about “plugins” and more about “composition of screens.”
- Philosophy: The application is a tree of ViewModels. A “Module” is just a child container that registers its own ViewModels and Services.
- Pros: Extremely natural if you are already doing MVVM. It handles the UI composition (View location) automatically.
- Cons: Tends to be more coupled to the specific MVVM framework.
In this architecture, you typically override SelectAssemblies in your Bootstrapper to tell Caliburn where to look for ViewModels.
The Problem: Dependency Hell#
Imagine this scenario:
- Host App uses
Newtonsoft.Jsonv13.0. - Plugin A uses
Newtonsoft.Jsonv13.0. (All good). - Plugin B is older and references
Newtonsoft.Jsonv9.0.
In a standard .NET application, you can only load one version of an assembly into the default context. When Plugin B tries to run, it might crash with a MethodNotFoundException because it got v13 instead of v9, or the load itself might fail.
The Solution: AssemblyLoadContext (ALC)#
Since .NET Core (.NET 5+), we have System.Runtime.Loader.AssemblyLoadContext. This allows us to load assemblies into isolated “islands.”
By loading each plugin into its own ALC, Plugin A can have its version of JSON.NET, and Plugin B can have its own version, side-by-side in the same process.
Implementing Isolation#
First, we need a custom context that knows how to resolve dependencies relative to the plugin’s location.
| |
Strategy 1: MEF with ALC#
To use this with MEF, we can’t just point a DirectoryCatalog at a folder. We need to manually load the assemblies into our ALC, and then pass those assemblies to MEF.
Note: This example uses
System.ComponentModel.Composition(MEF 1). If you are usingSystem.Composition(MEF 2 / Lightweight MEF), the container setup is slightly different (ContainerConfiguration), but the ALC principle remains the same.
| |
Strategy 2: Caliburn.Micro with ALC#
For Caliburn.Micro, we hook into the SelectAssemblies method in our Bootstrapper.
| |
Crucial Note for Caliburn: Since Caliburn relies heavily on reflection and type equality, you must ensure that the Contract Interfaces (e.g., IShell, IModule) are shared. They must be loaded in the Default Context (the host), not loaded again inside the Plugin Context.
The AssemblyDependencyResolver usually handles this correctly: if the host already has the assembly, it won’t resolve it from the plugin folder, allowing the runtime to fall back to the default context.
Conclusion#
- Use MEF if your application is data-centric or service-centric, and you need complex composition rules.
- Use Caliburn.Micro if your plugins are primarily UI screens (ViewModels/Views) and you want a seamless MVVM experience.
In either case, if you are distributing plugins that might have conflicting dependencies, wrapping them in an AssemblyLoadContext is the modern, robust way to keep your application stable.
Further Reading#
- MEF (Managed Extensibility Framework) - Official Microsoft documentation
- Caliburn.Micro Documentation - Official Caliburn.Micro docs
- AssemblyLoadContext - Understanding plugin isolation
- Plugin Architecture Patterns - Creating apps with plugin support
