MEF加载跨AppDomain的ApiController时出现‘Cannot serialize’错误
解决ASP.NET Web API + MEF跨AppDomain加载插件的序列化错误问题
我之前在做类似的.NET Framework 4.5 Web API动态插件更新需求时,也踩过这个「Cannot serialize」的大坑!本质原因是跨AppDomain传递的对象不满足.NET的远程通信要求,再加上ApiController本身的设计就不是为跨域实例传递准备的,直接用MEF导出控制器实例肯定会出问题。下面给你一步步拆解解决方案:
核心问题分析
当你把插件加载到独立AppDomain时,MEF尝试把导出的ApiController实例传递回主AppDomain,但:
ApiController没有标记[Serializable],也没有继承MarshalByRefObject,无法跨AppDomain序列化/远程调用;- 即使强行序列化,ApiController内部依赖的HttpContext、Configuration等对象都是和当前AppDomain绑定的,传递到主AppDomain后也无法正常工作。
所以我们的思路要从「跨域传递控制器实例」转变为「跨域传递请求处理逻辑的代理」。
具体解决方案步骤
1. 抽离共享契约层
首先创建一个独立的类库(比如PluginContracts.dll),这个类库不随插件更新,主AppDomain和所有插件都要引用它。在这里定义跨AppDomain通信的核心类型:
- 封装请求/响应的数据对象(必须标记
[Serializable]); - 定义跨域调用的接口(必须继承
MarshalByRefObject,这样.NET会自动生成远程代理)。
示例代码:
using System; using System.Collections.Generic; using System.Net; using System.Web.Http; namespace PluginContracts { [Serializable] public class PluginRequest { public string ControllerName { get; set; } public string ActionName { get; set; } public IDictionary<string, object> ActionParams { get; set; } } [Serializable] public class PluginResponse { public HttpStatusCode StatusCode { get; set; } public object Content { get; set; } public IDictionary<string, string> ResponseHeaders { get; set; } } // 跨AppDomain的请求处理接口,必须继承MarshalByRefObject public interface IPluginRequestProcessor : MarshalByRefObject { PluginResponse ProcessRequest(PluginRequest request); IEnumerable<string> GetAvailableControllers(); } }
2. 调整插件的MEF导出逻辑
插件里不要直接导出ApiController,而是导出实现IPluginRequestProcessor的类,由这个类负责在插件AppDomain内管理控制器实例并处理请求:
using System.Collections.Generic; using System.Linq; using System.Net; using System.Reflection; using System.Web.Http; using System.ComponentModel.Composition; using System.ComponentModel.Composition.Hosting; using PluginContracts; namespace MyPlugin { [Export(typeof(IPluginRequestProcessor))] public class PluginRequestProcessor : IPluginRequestProcessor { private readonly CompositionContainer _mefContainer; // MEF通过构造函数注入容器 [ImportingConstructor] public PluginRequestProcessor(CompositionContainer container) { _mefContainer = container; } public PluginResponse ProcessRequest(PluginRequest request) { // 从MEF容器获取目标控制器实例 var controller = _mefContainer.GetExportedValues<ApiController>() .First(c => c.GetType().Name.Equals(request.ControllerName, StringComparison.OrdinalIgnoreCase)); // 找到目标Action方法 var actionMethod = controller.GetType().GetMethod(request.ActionName, BindingFlags.Public | BindingFlags.Instance); // 执行Action并获取结果 var actionResult = actionMethod.Invoke(controller, request.ActionParams.Values.ToArray()); // 封装为跨域可传递的响应对象 return new PluginResponse { StatusCode = HttpStatusCode.OK, Content = actionResult, ResponseHeaders = new Dictionary<string, string>() }; } public IEnumerable<string> GetAvailableControllers() { return _mefContainer.GetExportedValues<ApiController>() .Select(c => c.GetType().Name); } } // 插件内的实际控制器,导出给插件内部的MEF容器 [Export(typeof(ApiController))] public class DynamicUserController : ApiController { public IHttpActionResult Get(int id) { return Ok(new { Id = id, Name = "Dynamic User " + id }); } } }
3. 主AppDomain的代理控制器与激活器
在主AppDomain里,我们需要:
- 创建并管理插件AppDomain;
- 通过
MarshalByRefObject代理调用插件的请求处理器; - 自定义
IHttpControllerActivator,创建本地代理控制器转发请求到插件AppDomain。
示例代码:
using System; using System.Net; using System.Threading; using System.Threading.Tasks; using System.Web.Http; using System.Web.Http.Controllers; using PluginContracts; namespace WebApiHost { public class PluginControllerActivator : IHttpControllerActivator { private readonly AppDomain _pluginAppDomain; private readonly IPluginRequestProcessor _requestProcessor; public PluginControllerActivator(string pluginDirectory) { // 创建插件AppDomain,设置应用基目录为插件所在路径 var domainSetup = new AppDomainSetup { ApplicationBase = pluginDirectory, PrivateBinPath = pluginDirectory }; _pluginAppDomain = AppDomain.CreateDomain("DynamicPluginDomain", null, domainSetup); // 从插件AppDomain创建请求处理器的远程代理 var processorType = typeof(IPluginRequestProcessor); _requestProcessor = (IPluginRequestProcessor)_pluginAppDomain.CreateInstanceAndUnwrap( processorType.Assembly.FullName, typeof(MyPlugin.PluginRequestProcessor).FullName); } public IHttpController Create(HttpRequestMessage request, HttpControllerDescriptor controllerDescriptor, Type controllerType) { // 返回本地代理控制器,负责转发请求到插件AppDomain return new ProxyApiController(_requestProcessor, controllerDescriptor.ControllerName); } } // 本地代理控制器,模拟ApiController的行为,实际请求转发到插件处理 public class ProxyApiController : ApiController { private readonly IPluginRequestProcessor _processor; private readonly string _controllerName; public ProxyApiController(IPluginRequestProcessor processor, string controllerName) { _processor = processor; _controllerName = controllerName; } public override Task<IHttpActionResult> ExecuteAsync(HttpControllerContext controllerContext, CancellationToken cancellationToken) { // 封装请求参数 var pluginRequest = new PluginRequest { ControllerName = _controllerName, ActionName = controllerContext.RouteData.Values["action"].ToString(), ActionParams = controllerContext.ActionDescriptor.GetParameters() .ToDictionary(p => p.ParameterName, p => controllerContext.ActionArguments[p]) }; // 调用插件AppDomain的处理器 var pluginResponse = _processor.ProcessRequest(pluginRequest); // 转换为Web API标准响应 var httpResponse = new HttpResponseMessage(pluginResponse.StatusCode) { Content = pluginResponse.Content != null ? new ObjectContent(pluginResponse.Content.GetType(), pluginResponse.Content, Configuration.Formatters.JsonFormatter) : null }; // 添加响应头 foreach (var header in pluginResponse.ResponseHeaders) { httpResponse.Headers.TryAddWithoutValidation(header.Key, header.Value); } return Task.FromResult<IHttpActionResult>(ResponseMessage(httpResponse)); } } }
4. 注册自定义激活器到Web API
在WebApiConfig里替换默认的控制器激活器:
using System.Web.Api; namespace WebApiHost { public static class WebApiConfig { public static void Register(HttpConfiguration config) { // 注册插件控制器激活器,指定插件目录 var pluginPath = @"C:\WebApiPlugins"; config.Services.Replace(typeof(IHttpControllerActivator), new PluginControllerActivator(pluginPath)); // 其他Web API配置(路由、格式器等) config.MapHttpAttributeRoutes(); config.Routes.MapHttpRoute( name: "DefaultApi", routeTemplate: "api/{controller}/{action}/{id}", defaults: new { id = RouteParameter.Optional } ); } } }
5. 插件更新的处理
当需要更新插件时,只需:
- 卸载旧的插件AppDomain:
AppDomain.Unload(_pluginAppDomain); - 删除旧插件程序集,复制新版本到插件目录;
- 重新创建新的AppDomain和请求处理器代理即可。
这样就彻底解决了旧程序集无法更新的问题,因为每个版本的插件都在独立的AppDomain里加载,卸载AppDomain后旧程序集就会被释放。
关键注意事项
- 契约层必须共享:主AppDomain和插件必须引用同一个版本的
PluginContracts.dll,否则会出现类型不匹配的问题; - 避免跨域传递复杂对象:所有跨AppDomain传递的数据都要标记
[Serializable],尽量用简单的DTO,不要传递依赖于AppDomain上下文的对象(比如HttpContext); - 资源清理:卸载AppDomain时要确保所有远程代理都被释放,避免内存泄漏;
- 异常处理:要在跨域调用时添加异常捕获,处理插件AppDomain崩溃或调用失败的情况。
内容的提问来源于stack exchange,提问作者sanjar
相关产品推荐
相关产品推荐

