MEF(Managed Extensibility Framework)是 .NET 框架中的扩展性框架,用于构建可扩展的应用程序。本文记录 MEF 在 .NET Framework 和 .NET Core/.NET 5+ 中的使用差异和实现方式,包括如何导出和导入组件、使用 ExportFactory 创建实例,以及在不同 .NET 版本中的配置差异。
一、MEF 简介
MEF(Managed Extensibility Framework)是微软提供的一个扩展性框架,允许应用程序在运行时动态发现和组合组件。它通过声明式的导入/导出机制,实现了松耦合的组件架构。
1.1 核心概念
- Export(导出):使用
[Export] 特性标记组件,使其可以被其他组件使用
- Import(导入):使用
[Import] 或 [ImportMany] 特性标记依赖,自动注入导出的组件
- ExportFactory:用于延迟创建导出组件的实例,特别适用于需要创建多个实例的场景
- CompositionContainer:组合容器,负责发现和组合导出的组件
1.2 .NET Framework vs .NET Core/.NET 5+
在 .NET Framework 中,MEF 是内置的(System.ComponentModel.Composition)。而在 .NET Core/.NET 5+ 中,需要使用 System.Composition NuGet 包,API 略有不同。
二、模块代码实现
2.1 导出 Window 组件
使用 [Export] 特性导出 Window 组件,并使用 [ExportMetadata] 添加元数据信息。
1 2 3 4 5 6 7 8 9
| [Export(typeof(Window))] [ExportMetadata("name", "首页")] public partial class MainView : Window { public MainView() { InitializeComponent(); } }
|
说明:
[Export(typeof(Window))]:导出类型为 Window 的组件
[ExportMetadata("name", "首页")]:添加元数据,键为 “name”,值为 “首页”
- 元数据可以用于筛选和识别不同的导出组件
2.2 导入多个 Window 组件
.NET Framework 版本
在 .NET Framework 中,使用 System.ComponentModel.Composition 命名空间:
1 2 3 4 5 6 7 8 9
| using System.ComponentModel.Composition; using System.ComponentModel.Composition.Hosting;
public class WindowsCompos { [Export("extWindows")] [ImportMany(typeof(Window))] public IEnumerable<ExportFactory<Window, IDictionary<string, object>>> ExtWindows { get; set; } }
|
.NET Core/.NET 5+ 版本
在 .NET Core/.NET 5+ 中,需要先安装 System.Composition NuGet 包,并使用 System.Composition 命名空间:
1
| dotnet add package System.Composition
|
1 2 3 4 5 6 7 8 9
| using System.Composition; using System.Composition.Hosting;
public class WindowsCompos { [Export("extWindows")] [ImportMany] public IEnumerable<ExportFactory<Window, IDictionary<string, object>>> ExtWindows { get; set; } }
|
关键差异:
- 命名空间:.NET Framework 使用
System.ComponentModel.Composition,.NET Core 使用 System.Composition
- ImportMany 特性:.NET Framework 版本需要指定类型
[ImportMany(typeof(Window))],.NET Core 版本可以省略类型参数 [ImportMany]
- ExportFactory:两个版本都支持,但命名空间不同
ExportFactory 的作用:
ExportFactory<T, TMetadata> 允许延迟创建导出组件的实例。这对于需要创建多个实例的场景非常有用,例如在 WPF 应用中需要创建多个 Window 实例。
三、主程序代码实现
3.1 .NET Framework 版本
在 .NET Framework 中,使用 AssemblyCatalog 和 CompositionContainer:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| using System.ComponentModel.Composition.Hosting; using System.Reflection;
var extAss = Assembly.Load("MultiToolKit.WpfApp.Modules.Home");
var catalog = new AssemblyCatalog(extAss);
var container = new CompositionContainer(catalog);
container.ComposeParts(this);
var extWindowsList = container.GetExportedValue<IEnumerable<ExportFactory<Window, IDictionary<string, object>>>>( "extWindows");
|
说明:
- AssemblyCatalog:从指定程序集中发现导出组件
- CompositionContainer:组合容器,负责组合导入和导出
- ComposeParts:组合部件,满足导入需求(如果
WindowsCompos 是当前类的属性)
- GetExportedValue:获取导出的值,参数是导出契约名称
3.2 .NET Core/.NET 5+ 版本
在 .NET Core/.NET 5+ 中,使用 ContainerConfiguration 和 CompositionHost:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| using System.Composition.Hosting; using System.Reflection; using System.Runtime.Loader;
var executableLocation = Assembly.GetEntryAssembly().Location; var path = Path.GetDirectoryName(executableLocation);
var assemblies = Directory .GetFiles(path, "*.dll", SearchOption.AllDirectories) .Select(AssemblyLoadContext.Default.LoadFromAssemblyPath) .ToList();
var configuration = new ContainerConfiguration() .WithAssemblies(assemblies);
using var container = configuration.CreateContainer();
_extWindowsList = container.GetExports<ExportFactory<Window, IDictionary<string, object>>>();
|
说明:
- AssemblyLoadContext:用于加载程序集,.NET Core 中推荐使用
AssemblyLoadContext.Default
- ContainerConfiguration:配置容器,指定要扫描的程序集
- WithAssemblies:添加要扫描的程序集列表
- CreateContainer:创建组合容器
- GetExports:获取导出的组件列表(泛型方法,不需要指定契约名称)
3.3 使用导出的组件
获取到 ExportFactory 列表后,可以通过 CreateExport() 方法创建实例:
1 2 3 4 5 6 7 8 9 10 11 12
| foreach (var exportFactory in extWindowsList) { var window = exportFactory.CreateExport().Value; var metadata = exportFactory.Metadata; var windowName = metadata["name"] as string; window.Show(); }
|
四、完整示例
4.1 .NET Framework 完整示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| using System.ComponentModel.Composition; using System.ComponentModel.Composition.Hosting; using System.Reflection; using System.Windows;
public partial class App : Application { private IEnumerable<ExportFactory<Window, IDictionary<string, object>>> _extWindowsList;
protected override void OnStartup(StartupEventArgs e) { base.OnStartup(e); var extAss = Assembly.Load("MultiToolKit.WpfApp.Modules.Home"); var catalog = new AssemblyCatalog(extAss); var container = new CompositionContainer(catalog); var compos = new WindowsCompos(); container.ComposeParts(compos); _extWindowsList = compos.ExtWindows; foreach (var exportFactory in _extWindowsList) { var window = exportFactory.CreateExport().Value; var metadata = exportFactory.Metadata; var windowName = metadata["name"] as string; window.Title = windowName; window.Show(); } } }
|
4.2 .NET Core/.NET 5+ 完整示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| using System.Composition; using System.Composition.Hosting; using System.Reflection; using System.Runtime.Loader; using System.Windows;
public partial class App : Application { private IEnumerable<ExportFactory<Window, IDictionary<string, object>>> _extWindowsList;
protected override void OnStartup(StartupEventArgs e) { base.OnStartup(e); var executableLocation = Assembly.GetEntryAssembly().Location; var path = Path.GetDirectoryName(executableLocation); var assemblies = Directory .GetFiles(path, "*.dll", SearchOption.AllDirectories) .Select(AssemblyLoadContext.Default.LoadFromAssemblyPath) .ToList(); var configuration = new ContainerConfiguration() .WithAssemblies(assemblies); using var container = configuration.CreateContainer(); _extWindowsList = container.GetExports<ExportFactory<Window, IDictionary<string, object>>>(); foreach (var exportFactory in _extWindowsList) { var window = exportFactory.CreateExport().Value; var metadata = exportFactory.Metadata; var windowName = metadata["name"] as string; window.Title = windowName; window.Show(); } } }
|
五、注意事项
5.1 程序集加载
- .NET Framework:使用
Assembly.Load() 或 Assembly.LoadFrom() 加载程序集
- .NET Core:推荐使用
AssemblyLoadContext.Default.LoadFromAssemblyPath(),避免程序集加载冲突
5.2 依赖注入
- MEF 可以与依赖注入框架(如 Microsoft.Extensions.DependencyInjection)一起使用
- 在 .NET Core 中,可以考虑使用内置的 DI 容器替代 MEF,除非需要插件式架构
5.3 生命周期管理
ExportFactory 创建的实例需要手动管理生命周期
- 使用
using 语句确保资源正确释放
5.4 元数据访问
- 元数据是弱类型的,访问时需要进行类型转换
- 建议定义强类型的元数据接口以提高类型安全性
5.5 性能考虑
- 程序集扫描和组合过程会有一定的性能开销
- 对于大型应用程序,考虑延迟加载或缓存组合结果
六、最佳实践
6.1 使用强类型元数据
定义元数据接口:
1 2 3 4 5 6 7 8 9 10 11 12 13
| public interface IWindowMetadata { string Name { get; } string Description { get; } }
[Export(typeof(Window))] [ExportMetadata("Name", "首页")] [ExportMetadata("Description", "应用程序主窗口")] public partial class MainView : Window { }
|
使用强类型元数据:
1 2 3 4 5 6 7 8 9
| [ImportMany] public IEnumerable<ExportFactory<Window, IWindowMetadata>> ExtWindows { get; set; }
foreach (var exportFactory in ExtWindows) { var metadata = exportFactory.Metadata; var name = metadata.Name; }
|
6.2 错误处理
1 2 3 4 5 6 7 8 9 10 11
| try { container.ComposeParts(this); } catch (CompositionException ex) { foreach (var error in ex.Errors) { Console.WriteLine($"Composition Error: {error.Description}"); } }
|
6.3 条件导入
使用 [ImportMany] 和 LINQ 进行条件筛选:
1 2 3 4 5 6
| [ImportMany] public IEnumerable<ExportFactory<Window, IDictionary<string, object>>> ExtWindows { get; set; }
var homeWindow = ExtWindows .FirstOrDefault(ef => ef.Metadata["name"]?.ToString() == "首页");
|
七、总结
MEF 是一个强大的扩展性框架,适用于需要插件式架构的应用程序。在 .NET Framework 和 .NET Core/.NET 5+ 中的主要差异包括:
- ✅ 命名空间不同:.NET Framework 使用
System.ComponentModel.Composition,.NET Core 使用 System.Composition
- ✅ API 略有差异:.NET Core 版本使用
ContainerConfiguration 和 CompositionHost
- ✅ 程序集加载方式不同:.NET Core 推荐使用
AssemblyLoadContext
- ✅ 功能基本一致:核心的导入/导出机制在两个版本中都可用
对于新项目,如果不需要插件式架构,建议优先考虑使用 .NET Core 内置的依赖注入框架。如果需要动态加载和组合组件,MEF 仍然是一个不错的选择。
八、相关参考