依赖注入到核心





5.00/5 (17投票s)
了解依赖注入机制如何从 ASP.NET 演进到 ASP.NET Core
好吧,既然我最近写的博客大多是关于 asp.net core 的,你可能会想,我为什么要写一篇关于 asp.net core 官方文档中已经有的主题的博客呢。
嗯,在这篇文章中,我不仅会讨论如何在 .net core 中实现 DI
(依赖注入),还会讨论 DI 本身 (是什么,为什么,如何做)。这篇文章反映了我实际学习 (仍在学习) DI 的过程。所以,如果你不想遵循我的方法,或者不想深入了解 DI,那么我建议你遵循下面提供的 asp.net core 文档中的官方链接。
https://docs.asp.net/en/latest/fundamentals/dependency-injection.html
如果你正在阅读这段文字,那么你可能对与我一起深入了解 DI 的精彩世界感兴趣。那么,你准备好了吗?我想是的。让我们开始吧!
应用程序由不同的组件组成,这些组件相互耦合。耦合是好的,但严格耦合呢?那绝对是糟糕的。根据非常著名的软件设计原则 (简称 SOLID) 的第二条原则,
"软件实体 (组件/类/模块/函数) 应该是对扩展开放的,对修改关闭的 (可以通过抽象类/接口实现)。"
由于我们需要让应用程序随着时间的推移而具有可扩展性,因此我们需要确保应用程序中的不同组件之间是松耦合的。DI 是实现应用程序松耦合的一种方式。
在企业级应用中,我们经常处理多层应用程序。为了举例,我们假设我们有一个三层应用程序。
这基本上是一个 Web API 应用程序。所以,我所做的就是通过 API 将一些数据暴露给客户端 (表示层)。在这里,我们在客户端 (表示层) 使用 AngularJs。我们在业务层拥有 API 控制器,并在数据访问层拥有 Entity Framework 数据库上下文来处理脏活。
在这种情况下,我们不必担心表示层中的 DI,因为我们可以单独在客户端进行 DI。像 AngularJS
这样的框架提供了自己在客户端实现 DI 的方式。请注意,我说的是框架 (AngularJs, EmberJs, BackboneJs)
而不是库 (JQuery, Knockout)
。你可以使用库并创建自己的框架,让人们以你提供的方式实现 DI。
我把事情说清楚,这样你就不能以后说,“你没有在表示层做 DI”。DI 实际上只是一个概念和一种学习技巧,学习之后,你可以在任何你喜欢的应用程序框架中使用相同的概念和技巧。这可以是服务器端框架,也可以是客户端框架。所以,不要混淆。
今天我将向你展示如何在 ASP.NET Core
中实现依赖。那是服务器端。但在此之前,让我们看看我们应用程序的当前状态。让我们看一下业务层和数据访问层。
数据访问层的一个快照是,(不要试图理解代码)。
public class TodoRepository
{
private readonly TodoContext _context = new TodoContext();
public IEnumerable<Todo> GetAll()
{
return _context.Todos;
}
public void Add(Todo item)
{
_context.Todos.Add(item);
_context.SaveChanges();
}
public Todo Find(int id)
{
Todo todo = _context.Todos.AsNoTracking().FirstOrDefault(t => t.Id == id);
return todo;
}
public Todo Remove(int id)
{
Todo todo = _context.Todos.FirstOrDefault(t => t.Id == id);
_context.Todos.Remove(todo);
_context.SaveChanges();
return todo;
}
public void Update(int id, Todo item)
{
Todo todo = _context.Todos.FirstOrDefault(t => t.Id == id);
todo.Title = item.Title;
todo.IsDone = item.IsDone;
_context.SaveChanges();
}
}
请注意,在这里,每次调用 TodoRepository
类时,我们都会创建一个新的 TodoContext
实例。我们的 TodoRepository
作为包装器工作,最终会与 TodoContext
中定义的特定 DbSet
进行通信。这是我们 TodoContext
的样子,
class TodoContext : DbContext
{
public TodoContext() : base("TodoDbConnectionString")
{
}
public DbSet<Todo> Todos { get; set; }
}
注意:TodoDbConnectionString 是连接字符串的名称。
我们在业务层 (中间层) 有 API
控制器。如果你仍在尝试理解代码,那就别管了。我们在这里是为了学习 DI,而不是如何构建多层应用程序。
public class TodoController : ApiController
{
private readonly TodoRepository _todoRepository = new TodoRepository();
// GET: api/Todo
public IEnumerable<Todo> Get()
{
return _todoRepository.GetAll();
}
// GET: api/Todo/5
public Todo Get(int id)
{
return _todoRepository.Find(id);
}
// POST: api/Todo
public void Post([FromBody]Todo todo)
{
_todoRepository.Add(todo);
}
// PUT: api/Todo/5
public void Put(int id, [FromBody]Todo todo)
{
_todoRepository.Update(id, todo);
}
// DELETE: api/Todo/5
public void Delete(int id)
{
_todoRepository.Remove(id);
}
}
请注意,这里我们每次调用 Api
控制器时,又创建了一个新的 TodoRepository
实例。永远记住,创建新实例意味着将事物粘合在一起。而这正是我们在 TodoController
和 TodoRepository
类中所做的。如果我们改变主意,想在控制器中使用另一个存储库 (例如,一个可以对文件系统执行 CRUD 操作的存储库) 而不是当前的呢?同样,如果我们想在存储库中使用不同的数据访问库而不是当前的呢?如果我们这样做,当没人看到的时候,我们会这样做:
我们将进入那些单独的类,然后替换实例化为新的类型。但是这样做会违反 SOLID
设计原则的第二条规则。(在这里,我们不仅让我们的应用程序难以扩展,而且每次需求发生变化时我们都在修改它)。
举个例子,我们的客户改变了需求,现在他们希望我们从文件系统中读取和写入逗号分隔数据 (CSV
),而不是数据库。假设我搜索并下载了一个可以处理 CSV 文件的很棒的库。因此,我们有一个配置好的 CSV 库,可以与数据访问层中的 CSV 文件进行通信。
如果你跟上我的思路,你就会说,现在我们有了新的数据访问层,我们也需要一个新的存储库。对吗?正是如此!由于我们想执行完全相同的 CRUD 操作,但现在是针对 CSV 文件,我们的方法签名将与 TodoRepository
相同,但实现将不同 (因为我们现在使用了一个新的库而不是 EF
来与 CSV 文件通信)。让我们不要深入实现细节,而是创建一个存储库并声明方法。假设我们新存储库的名称是 TodoCSVRepository
。
public class TodoCSVRepository
{
private readonly SomeCSVLibary _someCSVLibrary = new SomeCSVLibary();
public IEnumerable<Todo> GetAll()
{
/* Use _someCSVLibrary library instance and get all the todo */
}
public void Add(Todo item)
{
/* Use _someCSVLibrary library instance to add a new todo */
}
public Todo Find(int id)
{
/* Use _someCSVLibrary library to find a todo by id */
}
public Todo Remove(int id)
{
/* Use _someCSVLibrary library to remove a todo by id */
}
public void Update(int id, Todo item)
{
/* Use _someCSVLibrary library to update a todo */
}
}
假设后来我们的客户再次改变了需求,现在他/她提供了一个新的库,可以与内存数据库系统通信。所以,我们需要另一个存储库 (TodoInMemoryepository)。但是等等!难道你没觉得每次我们创建一个新的存储库时,我们都在声明一组相同的方法,这些方法对我们首选的数据存储执行一些 CRUD
操作吗?在这里这不是什么大问题,但问题是每次客户改变需求时,我们都会创建一个新的存储库并在控制器中实例化该存储库 (修改代码,而不是让代码可扩展)。所以,让我们一劳永逸地停止修改控制器代码。我们必须处理这个罪魁祸首——实例化一个存储库,
private readonly TodoRepository _todoRepository = new TodoRepository();
我们可以通过为我们的存储库创建一个抽象来轻松解决这个问题。我的意思是,我们可以从存储库中提取一个 interface
(抽象),然后使用它而不是首选存储库的具体类型。由于每个存储库都有完全相同的方法集,我们可以从中提取一个接口,它基本上看起来像这样,
public interface ITodoRepository
{
void Add(Todo item);
Todo Find(int id);
IEnumerable<Todo> GetAll();
Todo Remove(int id);
void Update(int id, Todo item);
}
现在我们可以让存储库实现这个简单的接口 (抽象),如下所示,
TodoRepository.cs
public class TodoRepository : ITodoRepository { ... }
TodoCSVRepository.cs
public class TodoCSVRepository : ITodoRepository { ... }
通过这个简单的抽象,我们现在可以修改控制器代码,使其依赖于抽象而不是具体类型。还记得我们的罪魁祸首吗?它现在看起来应该像这样,
private readonly ITodoRepository _todoRepository = new TodoRepository();
还没完!我们仍然在实例化一个特定的具体存储库。你不能创建一个接口的实例,所以我们仍然依赖于一个特定的存储库。为什么不进一步修改呢?让我们创建一个 ITodoRepository
字段,并在控制器中像这样实例化它,
private readonly ITodoRepository _todoRepository;
public TodoController()
{
_todoRepository = new TodoRepository();
}
正如你所见,现在我们的控制器构造函数负责创建具体类型。但是我们可以将这个创建具体类型的任务委托给其他人,由他们来调用我们的控制器。我的意思是,如果有人想使用我们的控制器,他们必须在控制器构造函数中传递他们喜欢的具体存储库类型的实例作为参数。
private readonly ITodoRepository _todoRepository;
public TodoController(ITodoRepository todoRepository)
{
_todoRepository = todoRepository;
}
通过这种方式,我们可以将责任向上转移,使我们的控制器免受进一步的代码修改。这种设计 (通过构造函数从别处传递实例类型) 也被称为构造函数注入模式。
这也遵循 SOLID 设计原则的第五条原则 (依赖倒置原则),该原则指出,
"高层模块不应依赖于低层模块。两者都应依赖于抽象。"
一切看起来都不错,但此时你的程序将无法如预期那样运行,因为它期望在控制器中传递一个存储库类型的实例。所以,我们可以做一些纯粹的人工 DI (从默认构造函数传递一个具体存储库类型的实例),
private readonly ITodoRepository _todoRepository;
public TodoController() : this(new TodoRepository())
{
}
public TodoController(ITodoRepository todoRepository)
{
_todoRepository = todoRepository;
}
当我们想使用不同的存储库时,我们仍然需要修改控制器代码。但是我们可以做得更进一步,构建一个 组合根
,我们可以在其中进行这种类型的初始化。组合根是一个简单的类,当应用程序首次初始化时会被调用。在该类中,我们可以解析抽象的具体类型。
public class CompositionRoot : IHttpControllerActivator
{
public IHttpController Create(
HttpRequestMessage request,
HttpControllerDescriptor controllerDescriptor,
Type controllerType)
{
if (controllerType == typeof(TodoController))
return new TodoController(
new TodoRepository());
return null;
}
}
正如你在这里看到的,我们已经以这样一种方式配置了我们的 HTTPRequest,即如果有人请求 TodoController
,我们将实例化一个 TodoRepository
的新实例并将其传递给控制器的构造函数。同样,当我们需要时,我们可以将其更改为 TodoCSVRepository
或 TodoInMemoryepository
。现在我们有一个地方可以处理所有脏的类型初始化工作。在 Web API 项目中,我们必须在 Global.asax.cs
文件中注册这个组合根,如下所示,
GlobalConfiguration.Configuration.Services.Replace(typeof(IHttpControllerActivator),new CompositionRoot());
有一点需要记住,组合根的实现因框架而异。这里我们处理的是 Web API,所以这对于 MVC (可能工作) 和 WPF 应用程序不起作用。你必须找到一种方法来实现它们,但不要担心,因为互联网上有很多代码片段可以简化你为特定框架创建组合根的任务。告诉你,我是在阅读 Mark Seeman
的一篇博客后学会为 Web API 项目创建组合根的。这里是链接,
http://blog.ploeh.dk/2012/09/28/DependencyInjectionandLifetimeManagementwithASP.NETWebAPI/
这很好,因为我们只需要处理我们这个小型项目的一些微小依赖项。如果我有一个大型项目,其中散布着数百个依赖项呢?在这种情况下,组合根不是一个好主意。这就是为什么在企业级应用中,我们使用一个众所周知的 IoC (控制反转) 容器来简化我们的工作。IoC 容器可以递归地解析依赖项,并且配置起来也很容易。它们允许我们轻松地处理依赖注入的生命周期。
有许多 IoC 容器可用,其中大多数以略有不同的方式做着相同的事情。让我们在当前项目中也使用其中的一个。让我们选择 Autofac
,它在网上有很棒的文档。这里是你可以了解 Autofac 与 Web API
项目集成相关的所有信息的链接,
http://autofac.readthedocs.io/en/latest/integration/webapi.html
由于我们只是刚开始接触依赖注入的世界。我们将慢慢来。Web API 项目的 Autofac
库也可以从 Nuget
下载,
Install-Package Autofac.WebApi2
我已经将它下载到我的 Techtalkers.WEB
项目中。现在是时候配置它了。我在 App_Start
文件夹中创建了一个类,并添加了这个方法,我在其中这样配置了 Autofac,
public class AutofacConfig
{
public static void RegisterAutofac()
{
var builder = new ContainerBuilder();
var config = GlobalConfiguration.Configuration;
builder.RegisterApiControllers(Assembly.GetExecutingAssembly());
builder.RegisterType<TodoRepository>().As<ITodoRepository>().InstancePerRequest();
var container = builder.Build();
config.DependencyResolver = new AutofacWebApiDependencyResolver(container);
}
}
在这里,我们获取 HttpConfiguration
,它基本上是 GlobalConfiguration.Configuration
。注册了当前程序集 (Techtalkers.WEB
) 中所有可用的 Api 控制器。然后注册了 TodoRepository
,以便在请求范围内出现 ITodoRepository
时,可以提供它的一个新实例。请注意,我们可以为特定实例定义生命周期。这里的 InstancePerRequest()
将为每个请求创建一个新的 TodoRepository 单例实例,并在嵌套的父作用域和嵌套作用域之间共享。你可以通过使用 Autofac 提供的其他扩展方法来定义服务实例的不同生命周期。这实际上是所有 Ioc 容器中的一个很棒的功能。最后,我们构建了容器并将其传递给 AutofacWebApiDependencyResolver
。这个类基本上实现了 Web API 项目提供的 IDependencyResolver
接口。就是这样。从现在开始,Autofac 将为我们的项目解析所有依赖项。我们不必担心它将如何做到。关键是它会做到。如果你对原始实现感兴趣,可以查看 Autofac 的 GitHub 存储库。这是一个开源项目。这是 GitHub 存储库链接,
https://github.com/autofac/Autofac
接下来,我们必须在 Global.asax.cs
中调用 RegisterAutofac()
。在那里,Application_Start()
每次应用程序启动时都会被调用。所以,就像其他注册一样,我在那里注册了 RegisterAutofac()
方法。
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
GlobalConfiguration.Configure(WebApiConfig.Register);
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
RouteConfig.RegisterRoutes(RouteTable.Routes);
AutofacConfig.RegisterAutofac();
}
现在,我们可以轻松地测试我们的 TodoController
。我们不必担心实例化具体存储库,因为我们不再依赖于任何它们。相反,当我们处于测试状态时,我们可以使用模拟存储库。一个名为 Moq
的库可以帮助我们做到这一点。对 Get()
方法的一个简单测试如下所示,
public class TestTodoControlloer
{
[TestMethod]
public void GetAll_ShouldReturnAllTodos()
{
// Arrange
var mockRepository = new Mock<ITodoRepository>();
mockRepository.Setup(x => x.GetAll())
.Returns(new List<Todo>()
{
new Todo() {Id = 1, Title = "Test Item 1", IsDone = true},
new Todo() {Id = 2, Title = "Test Item 2", IsDone = true},
new Todo() {Id = 3, Title = "Test Item 3", IsDone = false}
});
var controller = new TodoController(mockRepository.Object);
// Act
var todoes = controller.Get();
// Assert
Assert.IsNotNull(todoes);
Assert.AreEqual(3, todoes.Count());
Assert.AreEqual(2, todoes.Count(t => t.IsDone));
}
}
正如你在这里看到的,我们不必创建一个具体的存储库类型来在控制器中传递它。我们可以简单地使用一个模拟对象。
现在我们对 DI 有了一些初步了解,我们可以探索 .net core 的精彩世界,看看 DI 在那里是如何实现的。.Net
Core 默认提供了一些最基本的功能来实现 DI。但大多数情况下,它足以满足拥有数百个依赖项的大型项目。目前,.NET Core 的默认 IoC 容器仅支持构造函数注入。
所以,就像我们用 Autofac 所做的那样,如果你想在 ITodoRepository
出现时提供一个具体的 TodoRepository
实例,你可以在 ConfigureServices()
方法中这样做,
services.AddScoped<TodoRepository, ITodoRepository>();
正如你所见,我们也可以同时定义实例生命周期。在这里,定义 Scoped
生命周期的服务意味着将为每个请求创建一个新的 TodoRepository
实例,并在嵌套作用域中共享。
另外两个服务生命周期是 Singleton
和 Transient
。它们可以像上面一样注册,
services.AddSingleton<TodoRepository, ITodoRepository >();
services.AddTransient<TodoRepository, ITodoRepository >();
在单例生命周期中,服务实例在首次请求时创建,在后续请求中,这些实例将在所有父作用域和嵌套作用域之间共享。
在瞬态生命周期中,为所有父作用域和嵌套作用域的每个请求都会创建新的服务实例。
如果你对内置功能不满意,也可以添加第三方 IoC 容器。Autofac 本身就可以与 .NET Core 一起使用。你可以在下面的链接中很好地了解如何将 Autofac 与 .NET Core 集成,
http://docs.autofac.org/en/latest/integration/aspnetcore.html
你可以在这个官方链接中了解更多关于 .NET Core 中依赖注入的信息。
https://docs.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection
好了,就是这样。我想你现在对依赖注入及其相关术语已经相当熟悉了。但在你认为你已经完成之前,让我再次提醒你,这些都是非常初级的文章。此外,我只谈了构造函数注入。但也有属性注入、方法注入、接口注入等等。我将把探索它们的任务留给你。除了生命周期管理,你还可以用 IoC 容器做很多事情,所以还有很多东西需要学习。现在去动手实践吧。