如何学习 .NET Core 和 C# 中的依赖注入
在Twitter上关注我,很高兴接受您对主题或改进的建议/Chris
本文是对依赖注入(DI)的介绍。我计划后续文章介绍更高级的场景。目前,我想先解释一下它是什么,如何使用它,并最终展示它如何帮助进行大量的测试。
什么是依赖注入
它是一种使类独立于其依赖项的编程技术。
英语?
我们不依赖于依赖项的具体实现,而是接口。这使得我们的代码更加灵活,我们可以轻松地在保持相同逻辑的情况下将一个具体实现替换为另一个。
参考
为什么要使用它
有很多优点:
- 灵活的代码,我们可以在不改变业务逻辑的情况下将一种实现切换为另一种实现。
- 易于测试,因为我们依赖于接口而非实现——我们可以更轻松地测试代码,而不必担心副作用。我们将在本文的后面展示这一点。
.NET Core 中的 DI - 内置
有一个内置的依赖注入容器,被许多内部服务使用,例如:
- 托管环境
- 配置
- 路由
- MVC
- 应用程序生命周期
- 日志记录
容器有时也被称为IoC,即控制反转容器。
总体思路是在应用程序启动时注册,然后在运行时需要时解析。
容器职责:
- 创建
- 处置
- IServiceCollection,注册服务,让 IoC 容器知道具体的实现。它应该用于解析哪个接口属于哪个实现。创建对象的方式可以简单到只是实例化一个对象,但有时我们需要的数据远不止这些。
- IServiceProvider,解析服务实例,实际上是查找哪个接口属于哪个具体实现并进行创建。
它生活在Microsoft.Extensions.DependencyInjection
。
注册什么
有一些明显的迹象。
- 此方法之外的生命周期?我们是否新建了服务?是否有任何服务可以在该方法的范围内生存?也就是说,它们是否是依赖项?
- 多个版本,这个服务可以有多个版本吗?
- 可测试性,理想情况下,你只想测试一个特定的方法。如果你的方法中有很多其他功能的代码,你可能想把它们移到一个专门的服务中。这些被移走的代码将成为该方法的依赖项。
- 副作用,这与上一点类似,但它强调了方法只做一件事的重要性。如果产生了副作用,例如访问网络资源、执行 HTTP 调用或进行 I/O 交互,则应将其放置在单独的服务中,并作为依赖项注入。
本质上,你最终会将代码移到专用服务中,然后通过构造函数将这些服务作为依赖项注入。你可能一开始的代码看起来像这样:
public void Action(double amount, string cardNumber, string address, string city, string name)
{
var paymentService = new PaymentService();
var successfullyCharged = paymentService.Charge(int amount, cardNumber);
if (successfullyCharged)
{
var shippingService = new ShippingService();
shippingService.Ship(address, city, name);
}
}
上述存在很多问题:
- 测试时出现不必要的副作用
PaymentService
,第一个问题是我们控制和的生命周期ShippingService
,因此在尝试测试时可能会引发副作用,即 HTTP 调用。 - 无法测试所有路径,我们无法真正测试所有路径,我们不能要求 PaymentService 做出不同的响应,以便我们可以测试所有执行路径
- 很难扩展,这个 PaymentService 是否会涵盖所有可能的付款方式,或者如果我们添加对PayPal或新型卡的支持等,我们是否需要在这种方法中添加大量条件代码来涵盖不同的付款方式?
- 未经验证的基元,例如
double
和string
。我们可以信任这些值吗?address
例如 是有效地址吗?
从以上内容可以看出,我们需要将代码重构得更易于维护、更安全。将大量代码转换为依赖项,并用更复杂的结构替换原语,是一个不错的选择。
结果可能看起来像这样:
class Controller
private readonly IPaymentService _paymentService;
private readonly IShippingService _shippingService;
public void Controller(
IPaymentService paymentService,
IShippingService shippingService
)
{
_paymentService = paymentService;
_shippingService = shippingService;
}
public void Action(IPaymentInfo paymentInfo, IShippingAddress shippingAddress)
{
var successfullyCharged = _paymentService.Charge(paymentInfo);
if (successfullyCharged)
{
_shippingService.Ship(ShippingAddress);
}
}
}
上面我们将 和PaymentService
都转换ShippingService
为在构造函数中注入的依赖项。我们还看到所有原语都被收集到复杂结构IShippingAddress
和中IPaymentInfo
。剩下的就是纯粹的业务逻辑。
依赖图
当你有一个依赖项时,它本身可能依赖于另一个依赖项先被解析,依此类推。这意味着我们得到了一个依赖关系的层次结构,这些依赖项需要按照正确的顺序解析才能正常工作。我们称之为依赖图。
DEMO - 注册服务
我们将采取以下措施:
- 创建.NET Core 解决方案
- 在我们的解决方案中添加webapi 项目
- 失败,看看如果我们忘记注册服务会发生什么。识别错误信息很重要,这样我们才能知道哪里出错了,并修复它。
- 注册一个服务,我们将注册我们的服务,现在我们将看到一切是如何运作的
创建解决方案
mkdir di-demo
cd di-demo
dotnet new sln
这将创建以下结构:
-| di-demo
--------| di-demo.sln
创建 WebApi 项目Create a WebApi project
dotnet new webapi -o api
dotnet sln add api/api.csproj
上述操作将创建一个webapi
项目并将其添加到我们的解决方案文件中。
现在我们有以下结构:
-| di-demo
--------| di-demo.sln
--------| api/
失败
首先,我们将编译并运行我们的项目,因此我们输入:
dotnet run
首次运行项目时,Web 浏览器可能会提示类似“您的连接不安全”之类的信息。您的开发证书不受信任。幸运的是,有一个内置工具可以解决这个问题,您可以运行如下命令:
dotnet dev-certs https --trust
有关该问题的更多背景信息:
https://www.hanselman.com/blog/DevelopingLocallyWithASPNETCoreUnderHTTPSSSLAndSelfSignedCerts.aspx
你应该运行类似这样的程序:
好的,我们没有错误,但让我们引入一个错误。
让我们执行以下操作:
- 创建一个支持获取产品的控制器,这应该注入一个
ProductsService
- 创建一个 ProductsService,它应该能够从数据源检索产品
- **创建一个IProductsService接口,并在控制器中注入该接口
添加 ProductsController
ProductsController.cs
添加包含以下内容的文件:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Services;
namespace api.Controllers
{
[ApiController]
[Route("[controller]")]
public class ProductsController : ControllerBase
{
private readonly IProductsService _productsService;
public ProductsController(IProductsService productsService) {
_productsService = productsService;
}
[HttpGet]
public IEnumerable<Product> GetProducts()
{
return _productsService.GetProducts();
}
}
}
注意我们如何在构造函数中注入IProductsService
。该文件应该添加到Controllers
目录中。
添加 ProductsService
让我们ProductsService.cs
在目录下创建一个文件Services
,其内容如下:
using System;
using System.Collections.Generic;
using System.Linq;
namespace Services {
public class Product {
public string Title { get; set; }
}
public class ProductsService: IProductsService
{
private readonly List<Product> Products = new List<Product>
{
new Product { Title= "DVD player" },
new Product { Title= "TV" },
new Product { Title= "Projector" }
};
public IEnumerable<Product> GetProducts()
{
return Products.AsEnumerable();
}
}
}
创建接口 IProductsService
IProductsService.cs
让我们在目录下创建文件Services
,内容如下:
using System;
using System.Collections.Generic;
namespace Services
{
public interface IProductsService
{
IEnumerable<Product> GetProducts();
}
}
跑步
让我们使用以下命令运行该项目:
dotnet build
dotnet run
我们应该在浏览器中得到以下响应:
正如我们计划的那样,它失败了。现在怎么办?
好吧,我们通过将其注册到我们的容器中来修复它。
注册服务
好的,让我们来解决问题。我们在项目根目录中打开该文件Startup.cs
。找到该ConfigureServices()
方法。它目前应该有以下实现:
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
}
我们将代码改成如下形式:
public void ConfigureServices(IServiceCollection services)
{
services.AddTransient<IProductsService, ProductsService>();
services.AddControllers();
}
调用services.AddTransient()
注册器IProductsService
并将其与实现类关联起来ProductsService
。如果我们再次运行代码:
dotnet run
现在你的浏览器应该正常运行并且看起来像这样:
我们现在知道所有需要知道的事情了吗?
不,还有很多内容需要了解。请继续阅读下一节,了解不同的生命周期,transient只是生命周期类型的一种。
服务生命周期
服务生命周期是指服务在被垃圾回收之前可以存活的时间。目前有三种不同的生命周期:
- 瞬态,
services.AddTransient()
每次请求时都会创建该服务 - 单例模式,
services.AddSingleton()
在应用程序的整个生命周期内创建一次 - 作用域,
services.AddScoped()
,每个请求创建一次
那么什么时候使用每种类型?
好问题。
瞬态
因此,对于 Transient 来说,当你的状态可变,并且服务使用者需要获取该服务的副本时,使用 Transient 是有意义的。当线程安全不是必需的时,也是如此。当你不知道要使用什么样的生命周期时,这是一个很好的默认选择。
单例模式
单例模式意味着在应用程序的整个生命周期内只有一个实例。如果我们想要共享状态,或者创建服务的成本很高,并且我们只想创建一次,那么单例模式就非常适合。由于服务只创建一次,并且只被垃圾回收一次,因此可以提升性能。由于它可以被多个消费者访问,因此线程安全是一个需要考虑的问题。内存缓存就是一个很好的用例,但请确保它是线程安全的。点击此处了解更多关于如何实现线程安全的信息:
特别阅读lock
上面链接中的关键词。
范围
作用域意味着它在每个请求中只创建一次。因此,该请求中的所有调用者都将获得相同的实例。作用域服务的示例包括实体框架。它是我们用来访问数据库的类。将其设置为作用域DbContext
是有意义的。我们可能会在请求过程中多次调用它,因此资源的作用域应该限定于特定的请求/用户。
这里有龙
存在所谓的“捕获依赖项”。这意味着服务的寿命比预期的要长。
那么这为什么不好呢?
好吧,您希望服务能够按照其生命周期生存,否则,我们会占用不必要的内存空间。
这是怎么发生的?
当你开始依赖一个生命周期比你自身更短的服务时,你实际上是在捕获它,强制它按照你的生命周期继续存在。例如:
你注册一个ProductsService
具有作用域生命周期的 ,以及一个ILogService
具有瞬时生命周期的 。然后你将 注入到构造函数ILogService
中ProductsService
,从而捕获它。
class ProductsService
{
ProductsService(ILogService logService)
{
}
}
不要那样做!
如果你要依赖某些东西,请确保你注入的依赖项的生命周期与你自身的生命周期相同或更长。所以,要么改变你依赖的内容,要么改变依赖项的生命周期。
概括
我们解释了什么是依赖注入以及为什么使用它是个好主意。此外,我们还展示了内置容器如何帮助我们注册依赖项。最后,我们讨论了依赖项的生命周期是如何变化的,以及我们应该选择哪一种。
这是内置容器的第一部分。希望您能期待后续的文章,了解它的一些更高级的功能。
文章来源:https://dev.to/dotnet/how-you-can-learn-dependency-injection-in-net-core-and-c-245g