在构建模块化的ASP.NET MVC插件时,经常面临如何高效地整合第三方库的问题。幸运的是,有一些优雅的IoC框架,如Autofac,可以帮助减轻这种痛苦。Autofac甚至进一步开发了一些库来与ASP.NET集成,为节省了大量的时间。本文的目的是将IoC和DI特性通过Autofac集成到ASP.NET MVC插件框架中,这种解决方案同样适用于其他库的集成。
之前发表了一篇名为《ASP.NET MVC插件框架》的文章,展示了如何构建模块化的ASP.NET MVC插件。插件框架的设计初衷是能够轻松地整合第三方库,这正是插件框架的目的。因此,决定基于Autofac构建一个IoC插件。
在深入之前,让先开发一个使用IoC插件的示例。完整的源代码可以在这里下载,请选择“OSGi.NET Integration with Asp.NET MVC 3”或“OSGi.NET Integration with Asp.NET MVC 4”,两者都可以。
以“MediaPlugin”为例,需求是从数据存储(数据库或其他地方)加载所有流行的电影,然后在页面上显示。因此,创建了一个电影数据访问接口,其定义如下:
public interface IMoviceManager
{
List<Movie> GetMovies();
}
其次,创建了一个名为“MediaManagement”的插件,它实际上是数据访问层,其模拟实现如下:
public class MovieManager : IMoviceManager
{
private static List<Movie> _movies;
static MovieManager()
{
_movies = new List<Movie>();
_movies.Add(new Movie() { Name = "The Breaking Bad", Rating = 5 });
_movies.Add(new Movie() { Name = "The Avatar", Rating = 5 });
_movies.Add(new Movie() { Name = "The Walking Dead", Rating = 4 });
}
public List<Movie> GetMovies()
{
return _movies;
}
}
在插件激活器中,像这样将数据访问层注册到Autofac中:
public class Activator : IBundleActivator
{
public void Start(IBundleContext context)
{
var builder = context.GetFirstOrDefaultService<ContainerBuilder>();
builder.RegisterType<MovieManager>().AsImplementedInterfaces();
}
public void Stop(IBundleContext context)
{
}
}
OSGi.NET中的激活器是插件的入口。当插件处于活动状态时,将调用Start方法,如果变为非活动状态,则调用Stop方法,这使用户有机会进行资源准备和释放。激活器是可选的,点击这里可以学习一个简单的激活器示例。
现在数据访问插件已经准备好了,下一步是创建一个“MediaPlugin”来在网页上显示电影。
页面的控制器定义如下:
public class PopularTVShowController : Controller
{
private readonly IMoviceManager _moviceManager;
public PopularTVShowController(IMoviceManager moviceManager)
{
_moviceManager = moviceManager;
}
public ActionResult Index()
{
return View(_moviceManager.GetMovies());
}
}
当访问其视图时,控制器会自动构建。运行模式的屏幕截图如下:
为了帮助更好地理解自动注入机制,发布了调试屏幕截图如下:
从调用堆栈中可以看到自定义了一个ControllerFactory在插件框架中,它负责创建控制器实例。以下是它工作的主要内容步骤:
假设用户访问插件页面,URL是http://localhost/MediaPlugin/PopularTVShow/Index;自定义ControllerFactory从URL中识别出插件名称是MediaPlugin,控制器名称是PopularTVShow;自定义ControllerFactory启动MediaPlugin(如果它处于非活动状态),然后从插件程序集中解析Controller类型;ControllerFactory尝试加载ControllerResolver服务来构建控制器实例。这是IocPlugin的关键,因为ControllerResolver服务是由IocPlugin提供的。如果ControllerFactory找不到可用的ControllerResolver服务,它将调用System.Activator.CreateInstance代替。
这里没有花招,首先简单地创建一个空插件。当插件处于活动状态时,构建Autofac ContainerBuilder,然后将实例注册到插件框架中。这个项目中的插件框架是由OSGi.NET支持的,注册如下:
public static ContainerBuilder Initialize(this BundleRuntime runtime)
{
var containerBuilder = new ContainerBuilder();
runtime.AddService(typeof(ContainerBuilder), containerBuilder);
return containerBuilder;
}
然后唯一需要做的事情是监控任何插件变化,并维护ContainerBuilder的程序集。注册程序集的代码片段如下:
public static void SafeRegisterControllers(this ContainerBuilder containerBuilder, Assembly[] assmblies)
{
lock (containerBuilder)
{
var container = BundleRuntime.Instance.GetFirstOrDefaultService<IContainer>();
if (container == null)
{
containerBuilder.RegisterControllers(assmblies);
}
else
{
ContainerBuilder anotherBuilder = new ContainerBuilder();
anotherBuilder.RegisterControllers(assmblies);
anotherBuilder.Update(container);
}
}
}
Autofac做了真正的工作。在这里创建的IoC插件实际上重用了Autofac集成库来处理控制器依赖注入。Autofac ContainerBuilder不是线程安全的,所以在更新之前必须锁定它。
解决方案是插件框架独立的。这个插件框架是基于ASP.NET MVC插件框架的,但不限于该框架。这个解决方案应该适用于所有插件框架,如MEF、Mono Addin,但可能需要实现监控插件Start/Stop的逻辑。
插件依赖性 & 解析。插件框架的一个好特性是任何插件都可以在任何时候被移除/停止/启动。让回顾一下创建的插件,会发现MediaPlugin依赖于IocPlugin,所以MediaPlugin只有在IocPlugin处于活动状态时才有效。在现实生活中,可能由于任何原因,IocPlugin暂时不可用,在这种情况下,所有MediaPlugin中的视图应该对最终用户不可见,否则用户将看到错误页面抱怨“No parameterless constructor defined for the object”。因此,应该明确告诉插件框架MediaPlugin依赖于IoCPlugin,所以一旦IocPlugin不可用,IocPlugin也不可用,这被称为插件框架中的依赖性 & 解析。
在OSGi.NET中,依赖性在插件清单文件中定义如下:
<?xml version="1.0" encoding="utf-8"?>
<Bundle xmlns="urn:uiosp-bundle-manifest-2.0" Name="MediaPlugin" SymbolicName="MediaPlugin" Version="1.0.0.0" InitializedState="Active">
<Activator Type="MediaPlugin.Activator" Policy="Immediate"/>
<Runtime>
<Assembly Path="bin\MediaPlugin.dll" Share="false"/>
</Runtime>
<Dependency BundleSymbolicName="UIShell.IoCPlugin" Resolution="Mandatory"/>
</Bundle>
如果使用其他插件框架,最好考虑这种情况。
对于企业应用程序,通常有数百个插件,所以启动插件的顺序至关重要,一些核心插件如DataAccessPlugin、AuthenticationPlugin通常比其他插件更早启动。建议开发者不要让他们插件依赖于启动顺序。IocPlugin对大多数应用程序来说并不是必需的,所以没有保证它总是先于其他插件启动。考虑以下场景: Plugin1启动了,它的控制器依赖于IocPlugin,就像MediaPlugin一样;IocPlugin启动了,它将监控任何插件活动/非活动;MediaPlugin启动了,所以IocPlugin收到了插件活动通知,然后加载MediaPlugin程序集到ContainerBuilder;看到哪里错了吗?Plugin1中的程序集被IocPlugin忽略了!一个好的IocPlugin设计应该能够处理这种情况,即加载在它之前启动的程序集。
请开发者记住一个好的实践:尽可能不要让插件依赖于启动顺序。
让总结一下迁移的关键步骤,创建一个带有Activator类的空插件项目,添加对第三方程序集的引用;在插件Activator中构建服务,然后将其注册到插件框架服务容器中。对于IocPlugin,创建ContainerBuilder,然后通过调用BundleRuntime.AddService(typeof(ContainerBuilder), containerBuilder)将其放入bundle服务容器中。所有其他插件都可以从服务容器中获取ContainerBuilder,并直接使用它。