第 005 集 - 依赖注入 - ASP.NET Core:从零到过度
在这篇文章/视频中,我们将继续探讨 ASP.NET Core 的基本概念,为构建应用程序做好准备。这次,我们将讨论依赖注入,它内置于 ASP.NET Core 的核心中(看看我做了什么?😆)。
您可以查看下一个视频来了解演练,但如果您喜欢快速阅读,请跳至书面综合。
整个系列的播放列表在这里。
简介
在上一集中,我们开始探讨开发 ASP.NET Core 应用程序时的一些重要核心概念,即Program
和Startup
类,并简要了解了依赖注入和中间件。在本集中,我们将更深入地探讨依赖注入。
从控制器中提取逻辑
为什么
通常我们不希望控制器包含太多逻辑,尤其是业务逻辑。理想情况下,控制器应该专注于 MVC/HTTP 特定的逻辑——接收客户端输入,将其传递给负责业务逻辑的组件,然后返回结果(传入视图或其他形式,例如 JSON 负载),并可能根据业务逻辑的输出包含不同的 HTTP 状态码。
当然,这并非强制性的。我见过一些文章和讨论,比如说,如果我们开发的是小型微服务,不妨绕过这种代码拆分方式,因为如果需要调整,我们只需重写服务即可。总之,我还是会采用更经典的拆分方法,只是想说明一下,这并非一成不变(因为没有什么是一成不变的,总是有多种方法可以实现)。
将业务逻辑与 Web 框架分离的好处包括能够在不受 Web 框架特性所增加的复杂性的情况下进行测试,以及允许使用具有不同“网关”(替代 Web 框架、桌面应用程序等)的相同逻辑组件。
新的类库
为了从控制器中提取业务逻辑,我们将采用传统的三层架构,其中 MVC 应用程序将作为表示层,提取的逻辑将构成业务层。目前还没有数据层,我们将来会讲到。
N 层架构可能不是现在的热门,但对于我们现在所做的事情来说,它符合我们的需求并且足够简单。
首先创建几个类库CodingMilitia.PlayBall.GroupManagement.Business
和CodingMilitia.PlayBall.GroupManagement.Business.Impl
。第一个库将包含业务逻辑客户端需要知道的契约/API,而后者将包含所述逻辑的实现。
在合同库中,我们需要模型(Group
在这种情况下仅为类)和IGroupService
接口。
public class Group
{
public long Id { get; set; }
public string Name { get; set; }
}
public interface IGroupsService
{
IReadOnlyCollection<Group> GetAll();
Group GetById(long id);
Group Update(Group group);
Group Add(Group group);
}
转到服务实现库,我们创建一个InMemoryGroupsService
实现接口的类IGroupsService
- 目前没有太多的业务逻辑,主要是 CRUD,但我们假设有😛
public class InMemoryGroupsService : IGroupsService
{
private readonly List<Group> _groups = new List<Group>();
private long _currentId = 0;
public IReadOnlyCollection<Group> GetAll()
{
return _groups.AsReadOnly();
}
public Group GetById(long id)
{
return _groups.SingleOrDefault(g => g.Id == id);
}
public Group Update(Group group)
{
var toUpdate = _groups.SingleOrDefault(g => g.Id == group.Id);
if (toUpdate == null)
{
return null;
}
toUpdate.Name = group.Name;
return toUpdate;
}
public Group Add(Group group)
{
group.Id = ++_currentId;
_groups.Add(group);
return group;
}
}
顾名思义,数据存储在内存中(甚至不是线程安全的),所以说它还远远达不到生产环境的水平,也只是轻描淡写。它和我们在控制器中的逻辑完全一样,只是被拉入了一个独立的类中。
回到 Web 应用程序项目,我们添加对新创建的库的引用,并重新设计以GroupsController
使用IGroupsService
而不是在其中实现逻辑。
[Route("groups")]
public class GroupsController : Controller
{
private readonly IGroupsService _groupsService;
public GroupsController(IGroupsService groupsService)
{
_groupsService = groupsService;
}
[HttpGet]
[Route("")]
public IActionResult Index()
{
return View(_groupsService.GetAll().ToViewModel());
}
[HttpGet]
[Route("{id}")]
public IActionResult Details(long id)
{
var group = _groupsService.GetById(id);
if (group == null)
{
return NotFound();
}
return View(group.ToViewModel());
}
[HttpPost]
[Route("{id}")]
[ValidateAntiForgeryToken]
public IActionResult Edit(long id, GroupViewModel model)
{
var group = _groupsService.Update(model.ToServiceModel());
if (group == null)
{
return NotFound();
}
return RedirectToAction("Index");
}
[HttpGet]
[Route("create")]
public IActionResult Create()
{
return View();
}
[HttpPost]
[Route("")]
[ValidateAntiForgeryToken]
public IActionResult CreateReally(GroupViewModel model)
{
_groupsService.Add(model.ToServiceModel());
return RedirectToAction("Index");
}
}
需要注意以下几点:
- 我们现在在构造函数中接收
IGroupsService
,因此我们可以在操作中使用它 ToViewModel
和ToServiceModel
扩展方法 - 由于表示层和业务层使用不同的模型集,我们需要在传递它们时进行转换
public static class GroupMappings
{
public static GroupViewModel ToViewModel(this Group model)
{
return model != null ? new GroupViewModel { Id = model.Id, Name = model.Name } : null;
}
public static Group ToServiceModel(this GroupViewModel model)
{
return model != null ? new Group { Id = model.Id, Name = model.Name } : null;
}
public static IReadOnlyCollection<GroupViewModel> ToViewModel(this IReadOnlyCollection<Group> models)
{
if (models.Count == 0)
{
return Array.Empty<GroupViewModel>();
}
var groups = new GroupViewModel[models.Count];
var i = 0;
foreach (var model in models)
{
groups[i] = model.ToViewModel();
++i;
}
return new ReadOnlyCollection<GroupViewModel>(groups);
}
}
我们可以直接在控制器中进行映射,但最终会被这些样板代码污染。我们也可以使用AutoMapper自动完成这项工作,但它有时会隐藏代码中的问题(例如,我们在使用它时无法确定属性的引用是否存在)。总而言之,我喜欢同事提出的这个扩展方法,因为它在控制器中提供了良好的可读性,所以我会采用它。
如果我们现在尝试运行这个程序,将会报错,因为我们现在期望IGroupsService
在控制器中获取一个,但我们还没有告诉框架如何获取它。为此,我们需要在依赖注入容器中注册服务实现。
使用内置容器
注册服务
在上一集中,我们了解了如何在内置依赖注入容器中注册服务,本集也一样。我们希望注册一个服务,InMemoryGroupsService
并且像之前提到的一样,希望它拥有单例类型的生命周期,这样它保存在内存中的数据就会在应用程序的整个生命周期内一直存在。
为此,我们需要在Startup
类中添加一行新内容:services.AddSingleton<IGroupsService, InMemoryGroupsService>();
。
现在我们可以运行该应用程序,它的行为与以前一样,只是内部组织不同。
改善组织
目前,我们不需要担心太多,因为类ConfigureServices
的方法中只有几行代码Startup
,但是当我们添加更多代码时会怎样呢?
保持整洁的一个好方法ConfigureServices
是创建扩展方法来IServiceCollection
注册一组相互关联的服务。考虑到这一点,IoC
我们在新创建的文件夹中添加了一个新类ServiceCollectionExtensions
。
namespace Microsoft.Extensions.DependencyInjection
{
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddBusiness(this IServiceCollection services)
{
services.AddSingleton<IGroupsService, InMemoryGroupsService>();
//more business services...
return services;
}
}
}
注意这里的命名空间“技巧”,使用与 相同的命名空间IServiceCollection
。这通常是为了简化扩展方法的可发现性,因为它允许我们直接跳转到ConfigureServices
方法,输入services.
并立即在智能感知中获取方法,而无需添加另一个using
。
现在ConfigureServices
我们可以用 替换我们之前添加的行services.AddBusiness();
。
使用第三方容器
ASP.NET Core 内置了依赖注入,这很好,但我们需要记住,开箱即用的容器是为了满足 ASP.NET Core 的内部需求而构建的,而不是为了满足在其上构建的应用程序的所有需求。
对于我们可能遇到的更高级的场景,我们可以使用可以提供这些功能的第三方 DI 容器。
为了验证这种可能性,我们将Autofac添加到项目中并进行一些尝试。目的不是深入探讨 Autofac 本身,而是了解如何在 ASP.NET Core 中使用它,并演示它所添加的额外功能。
更换容器
首先要添加几个 NuGet 包 -Autofac
和Autofac.Extensions.DependencyInjection
。
为了向 Autofac 注册依赖项,我们创建一个新类AutofacModule
(在IoC
文件夹中),该类继承自Module
并覆盖该Load
方法。
public class AutofacModule : Module
{
protected override void Load(ContainerBuilder builder)
{
builder
.RegisterType<InMemoryGroupsService>()
.As<IGroupsService>()
.SingleInstance();
}
}
上面的代码与我们在 中看到的代码相同AddBusiness
,但适应了 Autofac 的 API。现在我们需要将 Autofac 配置为容器。
//...
public IServiceProvider ConfigureServices(IServiceCollection services)
{
services.AddMvc();
//if using default DI container, uncomment
//services.AddBusiness();
// Add Autofac
var containerBuilder = new ContainerBuilder();
containerBuilder.RegisterModule<AutofacModule>();
containerBuilder.Populate(services);
var container = containerBuilder.Build();
return new AutofacServiceProvider(container);
}
//...
需要注意的第一个变化是ConfigureServices
方法不再是void
,而是返回。这是必要的,这样 ASP.NET Core 就会使用返回的服务提供程序,而不是根据接收到的参数IServiceProvider
构建自己的服务提供程序。IServiceCollection
方法末尾的 Autofac 配置代码是从 Microsoft 的依赖注入文档中简单复制粘贴的。
基本上,我们在这里做的事情是:
- 创建 Autofac 容器构建器
- 使用依赖配置注册我们创建的模块
IServiceCollection
使用 ASP.NET Core 组件放置的注册信息填充 Autofac 的容器- 构建容器
- 返回一个新的服务提供者,使用 Autofac 容器实现
再次运行应用程序,我们保持相同的功能。
使用其他容器功能
到目前为止,我们还没有使用 Autofac 的任何特定功能来证明这种改变是合理的,除非是性能问题(事实并非如此,至少在撰写本文时,内置容器似乎更快,请参见此处)。
仅出于论证的目的,让我们使用 Autofac 提供的额外功能:装饰器。
public class AutofacModule : Module
{
protected override void Load(ContainerBuilder builder)
{
builder
.RegisterType<InMemoryGroupsService>()
.Named<IGroupsService>("groupsService")
.SingleInstance();
builder
.RegisterDecorator<IGroupsService>(
(context, service) => new GroupsServiceDecorator(service),
"groupsService");
}
private class GroupsServiceDecorator : IGroupsService
{
private readonly IGroupsService _inner;
public GroupsServiceDecorator(IGroupsService inner)
{
_inner = inner;
}
public IReadOnlyCollection<Group> GetAll()
{
Console.WriteLine($"######### Helloooooo from {nameof(GetAll)} #########");
return _inner.GetAll();
}
public Group GetById(long id)
{
Console.WriteLine($"######### Helloooooo from {nameof(GetById)} #########");
return _inner.GetById(id);
}
public Group Update(Group group)
{
Console.WriteLine($"######### Helloooooo from {nameof(Update)} #########");
return _inner.Update(group);
}
public Group Add(Group group)
{
Console.WriteLine($"######### Helloooooo from {nameof(Add)} #########");
return _inner.Add(group);
}
}
}
装饰器实现IGroupsService
接口,在每次方法调用时写入控制台,然后将实际工作委托给IGroupsService
构造函数中获得的另一个实现。
为了注册装饰器,我们将InMemoryGroupsService
注册方式改为命名注册,然后RegisterDecorator
使用 lambda 表达式调用来构建装饰器,并将我们添加的名称添加到实际实现的注册中。我不能说我是这个 API 的忠实粉丝,但这就是我们目前的做法。
现在,如果我们重新运行应用程序,我们将保留与以前相同的功能,但是另外,如果我们查看控制台,我们会在日志中看到装饰器消息。
结尾
以上就是 ASP.NET Core 中依赖注入的快速概览。虽然不会太深入,但足以让我们为接下来的内容做好准备。之后,我们会根据需要介绍更多相关概念。
使用 Autofac 可以很好地介绍如何替换内置容器,但我并不完全认同它的 API,我可能会在将来替换它。
这篇文章的源代码在这里。
请发送您的任何反馈,以便接下来的帖子/视频可以更好,甚至调整到更有趣的主题。
感谢您的光临,cyaz!
鏂囩珷鏉ユ簮锛�https://dev.to/joaofbantunes/episode-005---dependency-injection---aspnet-core-from-0-to-overkill-50e7