Featured image of post MEF 导出的插件([Export])如何使用由 Microsoft DI 容器(IServiceProvider)管理的服务(如 ILogger、IConfiguration 等)?

MEF 导出的插件([Export])如何使用由 Microsoft DI 容器(IServiceProvider)管理的服务(如 ILogger、IConfiguration 等)?

在混合使用 MEF(插件发现) 和 Microsoft DI(核心服务管理) 的架构中,MEF 默认无法直接解析 DI 容器中的服务。但可以通过 “服务桥接(Service Bridge / Adapter)” 实现。

✅ 优势

  • 插件不依赖 MEF 或 DI 容器细节;
  • 主程序完全控制传入的服务;
  • 易于单元测试(可 mock IAnalyzerContext);
  • 无服务定位(Service Locator)反模式。

📁 项目结构(延续之前的模板)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
SharpBoxes.Host/
├── Services/
│   ├── Core/
│   │   ├── IHostLogger.cs
│   │   └── ConsoleLogger.cs
│   └── Extensibility/
│       ├── IAnalyzer.cs
│       ├── IAnalyzerContext.cs   ← 新增
│       ├── AnalyzerContext.cs    ← 新增
│       └── PluginManager.cs
└── Plugins/
    └── ExamplePlugin.dll

1. 定义上下文接口(IAnalyzerContext)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// Services/Extensibility/IAnalyzerContext.cs
using SharpBoxes.Host.Services.Core;

namespace SharpBoxes.Host.Services.Extensibility
{
    /// <summary>
    /// 分析器执行上下文,由主程序提供,包含所需服务
    /// </summary>
    public interface IAnalyzerContext
    {
        IHostLogger Logger { get; }
        string WorkingDirectory { get; }
        // 可扩展:IConfiguration, IFileService 等
    }
}

2. 实现上下文(AnalyzerContext)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
// Services/Extensibility/AnalyzerContext.cs
using SharpBoxes.Host.Services.Core;

namespace SharpBoxes.Host.Services.Extensibility
{
    /// <summary>
    /// 上下文实现(由主程序创建)
    /// </summary>
    public class AnalyzerContext : IAnalyzerContext
    {
        public IHostLoader Logger { get; }
        public string WorkingDirectory { get; }

        public AnalyzerContext(IHostLogger logger, string workingDirectory = null)
        {
            Logger = logger ?? throw new ArgumentNullException(nameof(logger));
            WorkingDirectory = workingDirectory ?? Environment.CurrentDirectory;
        }
    }
}

3. 更新分析器契约(IAnalyzer)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// Services/Extensibility/IAnalyzer.cs
namespace SharpBoxes.Host.Services.Extensibility
{
    public interface IAnalyzer
    {
        string Name { get; }
        
        /// <summary>
        /// 分析代码,通过上下文获取服务
        /// </summary>
        string Analyze(string code, IAnalyzerContext context);
    }
}

4. 插件实现(无需任何 DI/MEF 服务注入)

 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
// ExamplePlugin/MyAnalyzer.cs
using System.ComponentModel.Composition;
using SharpBoxes.Host.Services.Extensibility;

namespace ExamplePlugin
{
    [Export(typeof(IAnalyzer))]
    [ExportMetadata("Name", "Context-Based Analyzer")]
    [ExportMetadata("Version", "1.0.0")]
    [ExportMetadata("Author", "Zheng")]
    [ExportMetadata("Priority", 80)]
    public class MyAnalyzer : IAnalyzer
    {
        public string Name => "Context-Based Analyzer";

        // ✅ 不需要 [Import],不依赖 MEF 或 DI
        public string Analyze(string code, IAnalyzerContext context)
        {
            // 使用上下文中的服务
            context.Logger.Info($"[MyAnalyzer] Starting analysis of {code.Length} chars");

            // 可访问工作目录等上下文信息
            context.Logger.Info($"Working directory: {context.WorkingDirectory}");

            return $"Analyzed by {Name}: '{code}' has {code.Length} characters.";
        }
    }
}

插件完全解耦:只需引用 IAnalyzerIAnalyzerContext(可放在共享契约库中)。


5. PluginManager(无需改动)

 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
// PluginManager.cs(保持之前实现,不再需要传入 IServiceProvider)
using System.ComponentModel.Composition;
using System.ComponentModel.Composition.Hosting;
using System.Reflection;

namespace SharpBoxes.Host.Services.Extensibility
{
    public class PluginManager
    {
        private readonly CompositionContainer _container;

        public PluginManager(string pluginsDirectory = @".\Plugins")
        {
            if (!Directory.Exists(pluginsDirectory))
                Directory.CreateDirectory(pluginsDirectory);

            var catalog = new AggregateCatalog();
            catalog.Catalogs.Add(new AssemblyCatalog(Assembly.GetExecutingAssembly()));
            catalog.Catalogs.Add(new DirectoryCatalog(pluginsDirectory, "*.dll"));

            _container = new CompositionContainer(catalog);
        }

        public IEnumerable<Lazy<IAnalyzer, IAnalyzerMetadata>> GetAnalyzers()
        {
            return _container.GetExports<IAnalyzer, IAnalyzerMetadata>();
        }
    }
}

6. 主程序(创建上下文并调用)

 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
// Program.cs
using Microsoft.Extensions.DependencyInjection;
using SharpBoxes.Host.Services.Core;
using SharpBoxes.Host.Services.Extensibility;

class Program
{
    static void Main(string[] args)
    {
        // 1. 设置 DI
        var services = new ServiceCollection();
        services.AddSingleton<IHostLogger, ConsoleLogger>();
        var serviceProvider = services.BuildServiceProvider();

        // 2. 设置 MEF 插件管理
        var pluginManager = new PluginManager();
        var analyzers = pluginManager.GetAnalyzersByPriority().ToList();

        // 3. 创建上下文(从 DI 获取服务)
        var logger = serviceProvider.GetRequiredService<IHostLogger>();
        var context = new AnalyzerContext(logger, Environment.CurrentDirectory);

        // 4. 调用插件
        logger.Info("Running analyzers with context...");
        foreach (var lazyAnalyzer in analyzers)
        {
            try
            {
                var analyzer = lazyAnalyzer.Value;
                var result = analyzer.Analyze("var x = 42;", context);
                logger.Info($"[RESULT] {result}");
            }
            catch (Exception ex)
            {
                logger.Info($"[ERROR] Analyzer '{lazyAnalyzer.Metadata.Name}' failed: {ex.Message}");
            }
        }

        Console.ReadKey();
    }
}

🖨️ 输出示例

1
2
3
4
[INFO] Running analyzers with context...
[INFO] [MyAnalyzer] Starting analysis of 12 chars
[INFO] Working directory: C:\SharpBoxes.Host\bin\Debug
[INFO] [RESULT] Analyzed by Context-Based Analyzer: 'var x = 42;' has 12 characters.

🧪 单元测试示例(插件)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
// MyAnalyzerTests.cs
[TestClass]
public class MyAnalyzerTests
{
    [TestMethod]
    public void Analyze_ShouldReturnCodeLength()
    {
        // Arrange
        var mockLogger = new Mock<IHostLogger>();
        var context = new AnalyzerContext(mockLogger.Object);
        var analyzer = new MyAnalyzer();

        // Act
        var result = analyzer.Analyze("hello", context);

        // Assert
        Assert.IsTrue(result.Contains("5"));
        mockLogger.Verify(x => x.Info(It.IsAny<string>()), Times.Exactly(2));
    }
}

✅ 无需启动 MEF 或 DI,纯 C# 测试。


✅ 总结

特性 实现
插件无容器依赖 ✅ 仅依赖 IAnalyzerContext
服务安全传递 ✅ 由主程序控制
易于测试 ✅ 可 mock 上下文
兼容 .NET Framework 4.8 ✅ 使用 MEF 1 + Microsoft DI
支持元数据 + 延迟加载 Lazy<IAnalyzer, IAnalyzerMetadata>

为什么我要使用上下文的方式而不是把ServiceProvider作为参数传入?

这种方式 避免插件持有 IServiceProvider,更安全、更清晰、更易测试。

✅ 优势

  • 插件不依赖 MEF 或 DI 容器细节;
  • 主程序完全控制传入的服务;
  • 易于单元测试(可 mock IAnalyzerContext);
  • 无服务定位(Service Locator)反模式。
Built with Hugo
Theme Stack designed by Jimmy