如何在.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:
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
CompositionContainerto 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.
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:
- 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 - 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.
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
IPizzaProviderinstances 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.
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
AssemblyLoadContextto load assemblies lazily instead of loading all upfront.
内容的提问来源于stack exchange,提问作者A. Trivedi

