最简单的 ASP.NET Web API 项目,使用 Castle Windsor 实现 IoC/DI






4.94/5 (6投票s)
在 23 个相当简单的步骤中,创建最简单的 ASP.NET Web API 应用,该应用使用 Castle Windsor 进行 DI
准备就绪
你需要先学会爬行,才能学会行走;有时,在你学会行走之前,你可能需要在地上爬行数英里,直到最终获得一丝启示。
正如有人所说:“使用 DI 的人很快就会发现自己迷失在困惑的海洋中。”
在过去的三周里,我一直在努力理解和实现控制反转 (IoC) 和依赖注入 (DI)。为了写一篇在我开始这段旅程时希望拥有的文章,我将尝试使这个分步教程尽可能基础和易于理解,我们将构建一个尽可能简单的 ASP.NET Web API 项目,该项目使用 Castle Windsor 实现基本的 DI。
开始 / 调整心态
首先,让我们对 DI 进行一些概念上的背景介绍,解释它不仅仅是另一个缩写,也不是最新的骗术,用来哄骗毫无戒心的经理,让他们强加给疲惫不堪、疑神疑鬼的开发人员:依赖注入是一种编写接口而非实现的方式——换句话说,是针对接口或抽象而不是具体实现(类或“组件”)进行编码;这很好,因为它允许扩展性——你不会被“锁定”在某个特定的类中。
DI(从现在开始我将这样称呼依赖注入)与控制反转(从现在开始我将称为 IoC)密切相关。这意味着,一个类不再是实例化它自己首先声明的一个具体类,而是使用一个接受接口类型作为参数的构造函数,并将实现该接口的具体类传递给它。
闪回/预告
在进入分步讲解之前,快速概述一下需要在 Web API 项目中添加哪些代码来“Windsorize”它,我将列出基本过程
- 通过 NuGet 添加引用:Castle.Core 和 Castle.Windsor
- 修改 `\App_Start\WebApiConfig.cs`,以便用 DI 路由(使用 `WindsorCompositionRoot`,这是一个待添加的类)替换常规路由
- 以 DI 风格添加一个 Controller(构造函数带有接口参数)
- 添加一个 `DIInstallers` 文件夹,然后在其下添加一个 `RepositoriesInstaller` 类。在此类中,注册你需要的类(将接口映射到具体实现)
- 添加一个 `DIPlumbing` 文件夹,然后在其中添加一个 `WindsorCompositionRoot` 类
- 添加一个 `Model` 文件夹;在其下,添加一个模型类、一个存储库接口和一个存储库接口实现(具体类)
- 修改 `Global.asax.cs` 以使其对 DI 友好
- 在根目录下添加 `WindsorDependencyResolver`
现在我们将退一步,从头开始(这已经被别人确定为很好的起点)。
DI 与 Web API 的交汇之处
特别是对于 ASP.NET Web API,Controller(MVC 中的“C”,Web API 期望你使用的模式)通常有一个默认的(隐式)无参数构造函数。然而,在使用 DI 时,必须有一个带有该签名的显式构造函数。换句话说,而不是没有构造函数或有一个空的默认构造函数,像这样
private DuckbillRepository duckbillRepository;
public DuckbillController()
{
}
……你会得到类似这样的东西
private IDuckbillRepository _duckbillRepository;
public DuckbillController(IDuckbillRepository duckbillRepository)
{
if (duckbillRepository == null)
{
throw new ArgumentNullException("duckbillRepository is null");
}
_duckbillRepository = duckbillRepository;
}
现在可能很容易看出这样做的好处——你可以使用任何实现 `IDuckbillRepository` 接口的类来实例化一个 Controller。通常,ASP.NET Web API 生态系统会根据客户端传入的 URI “自动”实例化合适的 Controller。它期望一个无参数构造函数。那么,我们如何“改变规则”并使用带参数的构造函数而不是预期的呢?也就是说,我们如何用一个实现所需接口的类来实例化 Controller 呢?
答案是,我们必须拦截 Web API MVC“正常”的路由 Controller 请求的方式。你需要用一个特定于 DI 的路由机制来替换默认的路由机制。但这还不够——你还必须将具体类/“组件”映射到抽象/接口(如上面的 `IDuckbillRepository`)。DI 生态系统允许你通过将它们映射到 Controller 构造函数期望的接口类型来指定哪些类在起作用。你可以使用 Mark Seemann(以下简称“DI Whisperer”)所谓的“穷人 DI”,但他建议你使用 DI 容器(一个 DI 框架)来帮助自动化一些原本繁琐的步骤。有许多这样的 IoC/DI 框架,但我将在此讨论的是 Castle Windsor(“Windsor Castle”一词的反转显然是指 CW 提供的“控制反转”),以下简称“CW”——除非你想把它和“乡村西部”混淆。在不深入背景或理论的情况下,我现在将直接开始分步教程,讲解如何创建最简单的 ASP.NET Web API/MVC 项目,该项目通过 Castle Windsor 实现 DI。
有关背景信息,请参阅 The DI Whisperer 的博客,特别是 这篇 和 这篇。
如果你需要或想要深入了解 .NET 中的 DI(以及关于 Castle Windsor 和许多其他 DI/IoC 容器框架的材料),请在此处获取 DI Whisperer 的书籍 。
开始——添加 MRC 组件
- 在 Visual Studio(最好是 VS 2013,但以下内容在稍旧的版本中应该非常相似)中,选择文件 > 新建项目… > 已安装 > 模板 > Visual C# > Web > ASP.NET Web 应用程序 > 确定。在弹出的“新建 ASP.NET 项目”对话框中,选择“Web API”模板。也许 Web API 应用实际上应该被称为使用 MRC 模式而不是 MVC,其中“V”(代表“视图”)被“R”(代表“存储库”)替换。
- 无论如何,现在通过右键单击 `Models` 文件夹并选择“添加” > “类…”,添加一个 Model。将该类命名为“`DPlatypus`”(或者如果你不喜欢这种有毒脚趾哺乳动物,可以取其他名字)。
- 给这个新类一些属性,例如
namespace WebApplication974.Models { public class DPlatypus { public int Id { get; set; } public string Name { get; set; } } }
- 创建一个存储相关数据的地方,尚未创建的 Controller 将从此查询所需数据。再次右键单击 `Models` 文件夹,然后选择“添加” > “新建项…” > “已安装” > “接口”,并将其命名为 `IDPlatypusRepository`(或者任何你想要的名称,但为了方便起见,选择一个与你为 Model 类命名的名称相匹配的名称,例如“I” + <任何名称> + “Repository”)。
- 向其添加类似以下的代码
public interface IDPlatypusRepository { IEnumerable<DPlatypus> GetAll(); String Get(int id); DPlatypus Add(DPlatypus platypus); }
注意:通常,为了实现全部 CRUD 方法,你会拥有比这几个方法更多的(具体来说,你还将拥有 `Remove` 和 `Update` 方法),但请记住——我们正在尽可能简化它,以展示最基本的 ASP.NET Web API DI CW 组合。
- 再次右键单击 `Models` 文件夹,然后选择“添加” > “类…”,将其命名为“`DPlatypusRepository`”。
- 像这样添加代码
public class DPlatypusRepository : IDPlatypusRepository { private List<DPlatypus> platypi = new List<DPlatypus>(); private int _nextId = 1; public DPlatypusRepository() { Add(new DPlatypus { Name = "Donald" }); Add(new DPlatypus { Name = "GoGo" }); Add(new DPlatypus { Name = "Patty" }); Add(new DPlatypus { Name = "Platypup" }); Add(new DPlatypus { Name = "Platypop" }); Add(new DPlatypus { Name = "Platymop" }); Add(new DPlatypus { Name = "Platydude" }); Add(new DPlatypus { Name = "Platydudette" }); } public IEnumerable<DPlatypus> GetAll() { return platypi; } public String Get(int id) { var platypus = platypi.Find(p => p.Id == id); return platypus == null ? string.Empty : platypus.Name; } public DPlatypus Add(DPlatypus platypus) { if (platypus == null) { throw new ArgumentNullException("platypus"); } platypus.Id = _nextId++; platypi.Add(platypus); return platypus; } }
- 现在,添加 Controller 代码;右键单击 `Controllers` 文件夹并选择“添加” > “Controller…”。在“添加脚手架”对话框中,选择“Web API 2 Controller - Empty”并点击“添加”按钮。这将打开“添加 Controller”对话框。将其命名为“`DPlatypusController`”(或者…等等)
- 在你的 `Controller` 类中,添加此 `using` 语句(将“`WebApplication974`”替换为你项目的适当命名空间(项目名称)
using WebApplication974.Models;
继续——添加 IoC/DI (CW) 组件
- 添加一个持有 `IDPlatypusRepository` 实例的字段。
public class DPlatypusController : ApiController { private readonly IDPlatypusRepository _duckbillRepository; }
- 添加一个接受 `IDPlatypusRepository` 类型的构造函数
public DPlatypusController(IDPlatypusRepository duckbillRepository) { if (duckbillRepository == null) { throw new ArgumentNullException("duckbillRepository is null"); } _duckbillRepository = duckbillRepository; }
- 向 Controller 添加一个方法,该方法将返回 Platypus 项目的数量
[Route("api/DPlatypus/{id}")] public string GetDPlatypusNameById(int Id) { return _duckbillRepository.Get(Id); }
现在,这样设置后,对以下内容的调用:http://<IPAddress>:<port>/api/<ControllerName>/<IdVal>(例如,在本地机器上测试时:“https://:33181/api/DPlatypus/4”)……应该会返回相应的值,例如“`Platypup`”。
但是——我们仍然需要用 DI/CW 路由替换 ASP.NET Web API 的常规路由。所以,我们必须先“感染”几个文件,然后才能工作。甚至在那之前(遵循我刚发明的 FTF(“First Things First”)模式),我们需要将必要的 Castle 包安装到我们的项目中。所以
- 选择“工具”>“程序包管理器”>“管理解决方案的 NuGet 程序包”>“联机”,然后输入“`Castle.Core`”并安装该程序包;然后,在 `Castle.Core` 安装之后,用“`Castle.Windsor`”做同样的事情。
- 现在,将 `Global.asax.cs` 修改如下
using System; using System.Reflection; using System.Web.Http; using System.Web.Mvc; using System.Web.Optimization; using System.Web.Routing; using Castle.Windsor; using Castle.Windsor.Installer; using Castle.MicroKernel.Resolvers.SpecializedResolvers; namespace WebApplication974 { public class WebApiApplication : System.Web.HttpApplication { private static IWindsorContainer _container; protected void Application_Start() { ConfigureWindsor(GlobalConfiguration.Configuration); GlobalConfiguration.Configure(c => WebApiConfig.Register(c, _container)); FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters); RouteConfig.RegisterRoutes(RouteTable.Routes); BundleConfig.RegisterBundles(BundleTable.Bundles); } public static void ConfigureWindsor(HttpConfiguration configuration) { _container = new WindsorContainer(); _container.Install(FromAssembly.This()); _container.Kernel.Resolver.AddSubResolver(new CollectionResolver(_container.Kernel, true)); var dependencyResolver = new WindsorDependencyResolver(_container); configuration.DependencyResolver = dependencyResolver; } protected void Application_End() { _container.Dispose(); base.Dispose(); } } }
- 将 `App_Start\WebApiConfig.cs` 修改如下
using System.Web.Http; namespace WebApplication974 { using System.Web.Http.Dispatcher; using Castle.Windsor; using DIPlumbing; public static class WebApiConfig { public static void Register(HttpConfiguration config, IWindsorContainer container) { MapRoutes(config); RegisterControllerActivator(container); } private static void MapRoutes(HttpConfiguration config) { config.MapHttpAttributeRoutes(); config.Routes.MapHttpRoute( name: "DefaultApi", routeTemplate: "api/{controller}/{id}", defaults: new { id = RouteParameter.Optional } ); } private static void RegisterControllerActivator(IWindsorContainer container) { GlobalConfiguration.Configuration.Services.Replace(typeof(IHttpControllerActivator), new WindsorCompositionRoot(container)); } } }
- 在项目根目录中添加一个新类(右键单击项目并选择“添加” > “类…”),将其命名为“`WindsorDependencyResolver`”
- 使用此代码
using System; using System.Collections.Generic; using System.Linq; using System.Web.Http.Dependencies; using Castle.Windsor; using Castle.MicroKernel.Registration; using System.Web.Http; using Castle.MicroKernel.Lifestyle; namespace WebApplication974 { public class WindsorDependencyResolver : System.Web.Http.Dependencies.IDependencyResolver { private readonly IWindsorContainer _container; public WindsorDependencyResolver(IWindsorContainer container) { _container = container; } public IDependencyScope BeginScope() { return new WindsorDependencyScope(_container); } public object GetService(Type serviceType) { return _container.Kernel.HasComponent(serviceType) ? _container.Resolve(serviceType) : null; } public IEnumerable<object> GetServices(Type serviceType) { if (!_container.Kernel.HasComponent(serviceType)) { return new object[0]; } return _container.ResolveAll(serviceType).Cast<object>(); } public void Dispose() { _container.Dispose(); } } public class WindsorDependencyScope : IDependencyScope { private readonly IWindsorContainer _container; private readonly IDisposable _scope; public WindsorDependencyScope(IWindsorContainer container) { this._container = container; this._scope = container.BeginScope(); } public object GetService(Type serviceType) { if (_container.Kernel.HasComponent(serviceType)) { return _container.Resolve(serviceType); } else { return null; } } public IEnumerable<object> GetServices(Type serviceType) { return this._container.ResolveAll(serviceType).Cast<object>(); } public void Dispose() { this._scope.Dispose(); } } public class ApiControllersInstaller : IWindsorInstaller { public void Install(Castle.Windsor.IWindsorContainer container, Castle.MicroKernel.SubSystems.Configuration.IConfigurationStore store) { container.Register(Classes.FromThisAssembly() .BasedOn<ApiController>() .LifestylePerWebRequest()); } } }
- 右键单击你的项目,选择“添加” > “新建文件夹”,并将其命名为 `DIPlumbing`。
- 右键单击新文件夹,选择“添加” > “类…”,将其命名为“`WindsorCompositionRoot`”。
- 向其中添加此代码
using System; using Castle.Windsor; using System.Net.Http; using System.Web.Http.Controllers; using System.Web.Http.Dispatcher; namespace WebApplication974.DIPlumbing { public class WindsorCompositionRoot : IHttpControllerActivator { private readonly IWindsorContainer _container; public WindsorCompositionRoot(IWindsorContainer container) { _container = container; } public IHttpController Create( HttpRequestMessage request, HttpControllerDescriptor controllerDescriptor, Type controllerType) { var controller = (IHttpController)_container.Resolve(controllerType); request.RegisterForDispose( new Release( () => _container.Release(controller))); return controller; } private sealed class Release : IDisposable { private readonly Action _release; public Release(Action release) { _release = release; } public void Dispose() { _release(); } } } }
- 右键单击你的项目并选择“添加” > “新建文件夹”;将其命名为“`DIInstallers`”
- 右键单击 `DIInstallers` 文件夹并选择“添加” > “类…”,将其命名为“`RepositoriesInstaller`”
- 将此代码添加到 `RepositoriesInstaller`
using Castle.MicroKernel.Registration; using Castle.MicroKernel.SubSystems.Configuration; using Castle.Windsor; using WebApplication974.Models; namespace WebApplication974.DIInstallers { public class RepositoriesInstaller : IWindsorInstaller { public void Install(IWindsorContainer container, IConfigurationStore store) { container.Register( Component.For<IDPlatypusRepository>().ImplementedBy<DPlatypusRepository>().LifestylePerWebRequest()); } } }
正如德国人所说:“Und damit basta!” 或者,就像库尔特·冯内古特可能写的那样:“就这么定了。”
注意:有一个相关的技巧,涉及替换接口的具体实现映射 在这里。
冲刺/定格
为了你的教育和阅读乐趣,以下是各个部分执行的顺序(我在每个部分都设置了一个断点,以找出运行时顺序)
在浏览器中输入“查询字符串”之前
- Global.asax.cs
DIInstallers
\RepositoriesInstaller
WindsorDependencyResolver
- App_Start\WebApiConfig.cs
DIPlumbing
\WindsorCompositionRoot
在浏览器中输入“查询字符串”之后
Models
\DPlatypusRepository
Controllers
\DPlatypusController
骑马奔向苍白余晖
好了,差不多了;这似乎是很多“疯狂”的代码,也许吧,但信不信由你,Castle Windsor 和 ASP.NET Web API 框架在“后台”为你做了大部分工作。你现在可以运行该项目;来验证它确实有效。一旦项目的首页在浏览器中打开,打开另一个标签页,输入:https://:33181/api/DPlatypus/N(将端口号替换为 Visual Studio 自动为你项目分配的端口,并将“N”替换为 1 到 8 之间的数字,对应于存储库中添加的 8 个值)。然后,如果你将 N 替换为每个人最喜欢的数字 7,你将看到类似这样的内容
<string xmlns="http://schemas.microsoft.com/2003/10/Serialization/">Platydude</string>
所以:由于 Id 为 7 的 Platypus 项目是“Platydude”,所以它奏效了!
尽管可能有数百万人在热切地想知道八种经典鸭嘴兽的名字,但有无数十亿人真的不在乎。所以这个项目本身没有目的;但作为真正使用 Castle Windsor 进行 DI 的 Web API 应用的起点——它在这里大放异彩!你可以根据需要添加 Controller 和存储库,并根据需要扩展这个想法。
现在你知道如何使用 CW 创建一个使用 DI/IOC 的 ASP.NET Web API 应用了。有人要一碗字母汤吗?
我认为这是目前最简洁地展示如何完成所有这些工作的文章。我们在这过程中失去了很多“为什么”和“怎么做”——但请记住,提供这些绝非我的本意。此外,步骤的顺序可能不是最合乎逻辑的——但这并不重要,因为在所有代码都添加完毕之前,项目都无法正常运行。无论如何,我的意图(无论是否有附加的“ion”)是打破关于 C# ASP.NET Web API MVC/MRC IOC/DI w. CW 的最短文章的世界纪录。我希望赢得金鸭嘴兽奖(一只小鸭嘴兽的雕像*),如果我赢了(为什么不呢?),它将自豪地陈列在我的住所。
有关 ASP.NET 中 DI 的真正内幕,我再次推荐 The DI Whisperer 的书籍 和 他的博客文章,特别是 这篇 和 这篇。
整个最基本(这是一个技术术语,可以追溯到石器时代的代码,当时早期开发者会在裸露的山洞 AKA 住所里一边啃食剑齿虎骨头,一边进行编码**)项目作为下载附加到本文中。
* 注意:一只小鸭嘴兽确实被称为 platypup。
** “bare bones”(最基本的)这个正确且恰当的术语有时会被听者误解为“bear bones”(熊骨头)*** 但没有确凿的证据表明熊是站在我们现在岌岌可危地站立着的巨人的食物来源。
*** 这是 Illocution/Perlocution Impedance Mismatch Syndrome (IPIMS) 综合症失控的典型案例。
致谢
特别感谢 Adam Connelly。为什么?请参阅 他在这里的回答,他帮助我解决了我的 DI/CW 项目,在我找到他之前,它混乱不堪,在他伴随着《威廉·泰尔序曲》(又名《孤独的游侠主题曲》)的响起,骑马而来,将其纠正。