You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在.NET中实现插件架构?——解决接口实现类的动态加载与实例化问题

Great question! The plugin-based architecture you're building—where your core library defines an interface, and implementations are shipped as separate NuGet packages that users can opt into—is a classic scenario, and brute-force DLL scanning (like your reflection example) is definitely not the most efficient approach. Let's walk through better alternatives tailored for .NET:

1. Use .NET's Built-In MEF (Managed Extensibility Framework)

MEF is designed exactly for this kind of plugin scenario—it lets you define "exports" in your plugin assemblies and "imports" in your core library, without manually scanning every DLL.

How it works:

  • In your core library, define the interface (no special attribute required, though [InheritedExport] can auto-mark all implementations):
    public interface IPizzaProvider 
    {
        Pizza GetMePizza(object customizationParameter);
    }
    
  • In each plugin NuGet package, mark the implementation with [Export(typeof(IPizzaProvider))]:
    [Export(typeof(IPizzaProvider))]
    public class PizzaHutPizzaProvider : IPizzaProvider
    {
        public Pizza GetMePizza(object customizationParameter)
        {
            // Implementation logic here
        }
    }
    
  • In your core library, use a CompositionContainer to load only assemblies with matching exports:
    var catalog = new DirectoryCatalog(AppContext.BaseDirectory);
    var container = new CompositionContainer(catalog);
    
    // Retrieve all IPizzaProvider implementations
    var providers = container.GetExportedValues<IPizzaProvider>();
    

MEF handles the heavy lifting of locating relevant assemblies, so you skip scanning every DLL in the runtime folder.

2. Implement a .NET-Style SPI (Service Provider Interface)

If you prefer a pattern similar to Java's SPI, you can replicate it in .NET with a few simple conventions:

How to build it:

  1. Define a resource convention: Each plugin assembly includes a text file named META-INF/services/Your.Namespace.IPizzaProvider (match the full interface name). The file lists the full type name of the implementation, one per line:
    PizzaHut.Providers.PizzaHutPizzaProvider, PizzaHut.PizzaProvider
    
  2. Read these resources in your core library: Instead of scanning all DLLs, check assemblies for this resource file and load the specified types:
    var providers = new List<IPizzaProvider>();
    var interfaceFullName = typeof(IPizzaProvider).FullName;
    
    foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
    {
        var resourceName = $"META-INF/services/{interfaceFullName}";
        using (var stream = assembly.GetManifestResourceStream(resourceName))
        {
            if (stream == null) continue;
            
            using (var reader = new StreamReader(stream))
            {
                while (!reader.EndOfStream)
                {
                    var typeName = reader.ReadLine().Trim();
                    if (string.IsNullOrEmpty(typeName)) continue;
                    
                    var providerType = assembly.GetType(typeName);
                    if (providerType != null && typeof(IPizzaProvider).IsAssignableFrom(providerType))
                    {
                        providers.Add((IPizzaProvider)Activator.CreateInstance(providerType));
                    }
                }
            }
        }
    }
    

This way, you only process assemblies that explicitly declare they contain an implementation, avoiding unnecessary scanning.

3. Explicit Dependency Injection Registration (Most Controllable & Performant)

For a modern, .NET-idiomatic approach, have each plugin provide an extension method that registers its implementation with the DI container. This requires zero scanning and gives users full control over which plugins to enable.

How it works:

  • In each plugin NuGet package, add an extension method for IServiceCollection:
    public static class PizzaHutServiceExtensions
    {
        public static IServiceCollection AddPizzaHutProvider(this IServiceCollection services)
        {
            services.AddSingleton<IPizzaProvider, PizzaHutPizzaProvider>();
            return services;
        }
    }
    
  • Users add the NuGet package, then call the extension method in their app's startup:
    var builder = WebApplication.CreateBuilder(args);
    builder.Services.AddCorePizzaLibrary(); // Your core library registration
    builder.Services.AddPizzaHutProvider(); // Enable the plugin
    
  • Your core library can resolve all IPizzaProvider instances directly from the DI container:
    public class PizzaService
    {
        private readonly IEnumerable<IPizzaProvider> _providers;
        
        public PizzaService(IEnumerable<IPizzaProvider> providers)
        {
            _providers = providers;
        }
        
        // Use the providers as needed
    }
    

This is the most maintainable option—it integrates seamlessly with .NET's built-in DI and eliminates scanning entirely.

4. Optimized Reflection Scanning (If You Need Flexibility)

If you must use reflection, you can drastically reduce performance overhead by:

  • Scanning only DLLs with a specific naming convention (e.g., *PizzaProvider.dll):
    var dllPaths = Directory.GetFiles(AppContext.BaseDirectory, "*PizzaProvider.dll", SearchOption.TopDirectoryOnly);
    
  • Filtering types by a custom attribute (e.g., [PizzaProviderPlugin] on implementation classes):
    var providers = allAssemblies
        .SelectMany(x => x.GetTypes())
        .Where(t => typeof(IPizzaProvider).IsAssignableFrom(t) 
                    && t.IsClass 
                    && t.GetCustomAttribute<PizzaProviderPluginAttribute>() != null)
        .ToList();
    
  • Using AssemblyLoadContext to load assemblies lazily instead of loading all upfront.

内容的提问来源于stack exchange,提问作者A. Trivedi

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.27 18:23:14