基于 Autofac 构建 Asp.net 依赖注入、IoC 插件
介绍如何将 Autofac 引入 Asp.net 插件框架
引言
关于 Asp.net 依赖注入和 IoC,有很多优秀的文章,大多数解决方案看起来都很吸引人,但你几乎需要从头开始开发一切。
幸运的是,有一些优雅的 IoC 框架可以解决这些痛点,例如 Autofac 和 Spring.net. Autofac 更进一步,开发了一些库来 与 Asp.net 集成,这为我们节省了很多时间。
那么这篇文章是关于什么的呢?我不是在试图 reinvent 轮子,而是 **通过集成 Autofac,将 IoC & DI 功能引入 Asp.net 插件框架**,此解决方案也适用于集成其他库。
背景
我发布了一篇文章 **Asp.net MVC 插件框架** ,展示了构建模块化 Asp.net MVC 插件的解决方案。面向插件框架的设计应该能够轻松集成第三方库,这就是插件框架的作用。所以我决定基于 Autofac 构建一个 IoC 插件。
使用代码
在我们深入之前,让我们开始开发一个使用 IoC 插件的演示。完整的源代码可以 **在此下载**,请选择
OSGi.NET 集成 Asp.NET MVC 3
或
OSGi.NET 集成 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 中的 Activator 是插件的入口。当插件激活时,会调用 **Start** 方法;如果变为非活动状态,则调用 **Stop** 方法,这使用户有机会做一些事情,例如准备资源和释放资源。激活器是可选的,**点击此处 **了解一个简单的激活器演示。
目前,数据访问插件已准备就绪,下一步是创建一个 **MediaPlugin** 来在网页上显示电影。
页面的控制器定义如下:
public class PopularTVShowController : Controller
{
private readonly IMoviceManager _moviceManager;
/// <summary>
/// Create instance of HelloController。
/// </summary>
/// <param name="moviceManager">MoviceManager business layer implementation, which is injected by IoC.</param>
public PopularTVShowController(IMoviceManager moviceManager)
{
_moviceManager = moviceManager;
}
public ActionResult Index()
{
//
return View(_moviceManager.GetMovies());
}
}
访问其视图时,控制器会自动构造。运行模式截图如下:
它是如何工作的?
为了帮助我们更好地理解自动注入机制,我在此发布调试截图:
从调用堆栈可以看到,我们自定义了一个插件框架中的 ControllerFactory,它负责创建控制器实例。以下是其主要工作步骤:
- 假设用户访问一个插件页面,URL 为 https:///MediaPlugin/PopularTVShow/Index;
- 我们的自定义 ControllerFactory 从 URL 中识别出插件名称是 **MediaPlugin**,控制器名称是 **PopularTVShow**;
- Customized ControllerFactory 启动 **MediaPlugin**(如果它处于非活动状态),然后从插件程序集中解析 Controller 类型;
- ControllerFactory 尝试加载 **ControllerResolver** 服务来构造控制器实例。这是 **IocPlugin** 的关键,因为 **ControllerResolver** 服务是由 **IocPlugin** 提供的。如果 ControllerFactory 找不到可用的 **ControllerResolver** 服务,它将调用 System.Activator.CreateInstance 替代。
如何创建 IoC 插件?
这里没有技巧,只需先创建一个空的插件。当插件激活时,构造 Autofac ContainerBuilder,然后将其注册到你的插件框架。此项目中的插件框架由 **OSGi.NET** 支持,注册如下:
public static ContainerBuilder Initialize(this BundleRuntime runtime)
{
//provide the container builder so that each plugin can register the dependancy when starting.
var containerBuilder = new ContainerBuilder();
runtime.AddService(typeof(ContainerBuilder), containerBuilder);
return containerBuilder;
}
然后唯一需要做的是监控任何插件更改,并维护 **ContainerBuilder** 的程序集。注册程序集的代码如下:
public static void SafeRegisterControllers(this ContainerBuilder containerBuilder, Assembly[] assmblies)
{
//ContainerBuilder is not thread safe。
lock (containerBuilder)
{
var container = BundleRuntime.Instance.GetFirstOrDefaultService<IContainer>();
if (container == null)
{
//If container is newly created,it can accept assmblies right now.
containerBuilder.RegisterControllers(assmblies);
}
else
{
ContainerBuilder anotherBuilder = new ContainerBuilder();
anotherBuilder.RegisterControllers(assmblies);
anotherBuilder.Update(container);
}
}
}
兴趣点
Aotufac 完成了实际工作
我在这里创建的 IoC 插件实际上重用了 Autofac 集成库来处理控制器依赖注入。Autofac ContainerBuilder 不是线程安全的,因此在更新之前必须锁定它。
此解决方案独立于插件框架
此插件框架基于 ASP.NET MVC 插件框架, 但不限于该框架。此解决方案应适用于所有插件框架,如 MEF、Mono Addin,但你可能需要实现监控插件启动/停止的逻辑。
插件依赖与解析
插件框架的一个良好特性是任何插件都可以随时移除/停止/启动。让我们回顾一下我们创建的插件,你会发现 **MediaPlugin** 依赖于 **IocPlugin**,所以 **MediaPlugin** 只有在 IoCPlugin 激活时才能工作。在实际情况中,也许出于任何原因,IocPlugin 暂时不可用,在这种情况下,MediaPlugin 中的所有视图都应该对最终用户不可见,否则,用户将看到一个错误页面,抱怨“对象没有无参数构造函数”。因此,我们应该明确地告诉插件框架 **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" />
<Dependency BundleSymbolicName="UIShell.IoCPlugin" Resolution="Mandatory" />
</Runtime>
</Bundle>
如果你使用其他插件框架,最好考虑这种情况。
启动插件的顺序
对于企业应用程序,通常有数百个插件,因此启动插件的顺序至关重要,像 DataAccessPlugin、AuthenticationPlugin 这样的核心插件通常比其他插件更早启动。我建议开发者不要让他们自己的插件依赖于启动顺序。IocPlugin 对大多数应用程序来说不是必需的,因此不能保证它总是比其他插件更早启动。考虑以下场景:
- Plugin1 开始启动,其控制器与 **MediaPlugin** 一样依赖于 **IocPlugin**;
- IocPlugin 启动,它将监控任何插件的激活/非激活状态;
- MediaPlugin 启动,IocPlugin 收到插件激活通知,然后将 MediaPlugin 程序集加载到 ContainerBuilder;
你看出这里有什么问题了吗?**Plugin1** 中的程序集被 **IocPlugin** 忽略了!**IocPlugin** 的一个好的设计应该能够处理这种情况,即加载比它更早启动的程序集。
开发者请记住良好的实践:
尽可能不要让你的插件依赖于启动顺序。
将更多库迁移到插件中
让我们总结一下迁移的关键步骤:
- 创建一个带有 Activator 类的空插件项目,并添加对第三方程序集的引用;
- 在插件 Activator 中构造你的服务,并将其注册到插件框架服务容器。对于 **IocPlugin**,我们创建 ContainerBuilder,然后通过调用将其放入 bundle 服务容器
BundleRuntime.AddService(typeof(ContainerBuilder), containerBuilder);
所有其他插件都可以从服务容器获取 ContainerBuilder,并直接使用它。
有了这个解决方案,我们可以根据需要将尽可能多的库迁移到插件中。例如,我们可以
- 通过集成 ActiveMQ 或 ESB 来实现一个 消息代理 插件;
- 通过集成 log4net 或企业库来创建日志插件;
- 按需实现其他插件。
历史
无。