在这篇文章中,我们将深入研究.NET Core和ASP.NET Core MVC中的依赖注入,将介绍几乎所有可能的选项,依赖注入是ASP.Net Core的核心,我将分享在ASP.Net Core应用中使用依赖注入的一些经验和建议,并且将会讨论这些原则背后的动机是什么:

(1)有效地设计服务及其依赖关系。

(2)防止多线程问题。

(3)防止内存泄漏。

(4)防止潜在的错误。

在讨论该话题之前,了解什么是服务是生命周期至关重要,当组件通过依赖注入请求另一个组件时,它接收的实例是否对该组件实例是唯一的取决于生命周期。 因此,设置生存期决定了组件实例化的次数以及组件是否共享。

一、服务的生命周期

在ASP.Net Core 依赖注入有三种:

  • Transient :每次请求时都会创建,并且永远不会被共享。
  • Scoped : 在同一个Scope内只初始化一个实例 ,可以理解为( 每一个request级别只创建一个实例,同一个http request会在一个 scope内)
  • Singleton :只会创建一个实例。该实例在需要它的所有组件之间共享。因此总是使用相同的实例。

DI容器跟踪所有已解析的组件, 组件在其生命周期结束时被释放和处理:

  • 如果组件具有依赖关系,则它们也会自动释放和处理。
  • 如果组件实现IDisposable接口,则在组件释放时自动调用Dispose方法。

重要的是要理解,如果将组件A注册为单例,则它不能依赖于使用Scoped或Transient生命周期注册的组件。更一般地说:

服务不能依赖于生命周期小于其自身的服务。

通常你希望将应用范围的配置注册为单例,数据库访问类,比如Entity Framework上下文被推荐以Scoped方式注入,以便可以重用连接。如果要并行运行的话,请记住Entity Framework上下文不能由两个线程共享,如果需要,最好将上下文注册为Transient,然后每个服务都获得自己的上下文实例,并且可以并行运行。

建议的做法:

尽可能将您的服务注册为瞬态服务。 因为设计瞬态服务很简单。 您通常不用关心多线程和内存泄漏,并且您知道该服务的寿命很短。
1、请谨慎使用Scoped,因为如果您创建子服务作用域或从非Web应用程序使用这些服务,则可能会非常棘手。
2、谨慎使用singleton ,因为您需要处理多线程和潜在的内存泄漏问题。
3、在singleton 服务中不要依赖transient 或者scoped 服务,因为如果当一个singleton 服务注入transient服务,这个 transient服务就会变成一个singleton服务,并且如果transient服务不是为支持这种情况而设计的,则可能导致问题。 在这种情况下,ASP.NET Core的默认DI容器已经抛出异常。

二、注册服务:

注册服务是ConfigureServices(IServiceCollection)在您Startup班级的方法中完成的。

以下是服务注册的示例:

services.Add(new ServiceDescriptor(typeof(IDataService), typeof(DataService), ServiceLifetime.Transient));

该行代码添加DataService到服务集合中。服务类型设置为IDataService如此,如果请求该类型的实例,则它们将获得实例DataService。生命周期也设置为Transient,因此每次都会创建一个新实例。

ASP.NET Core提供了各种扩展方法,方便服务的注册,一下是最常用的方式,也是比较推荐的做法:

services.AddTransient<IDataService, DataService>();

简单吧,对于不同的生命周期,有类似的扩展方法,你可以猜测它们的名称。如果需要,你还可以注册单一类型(实现类型=服务类型)

services.AddTransient<DataService>();
services.AddTransient<DataService, DataService>();

在某些特殊情况下,您可能希望接管某些服务的实例化过程。在这种情况下,您可以使用下面的方法例子:

services.AddTransient<IDataService, DataService>((ctx) =>
{
IOtherService svc = ctx.GetService<IOtherService>();
//IOtherService svc = ctx.GetRequiredService<IOtherService>();
return new DataService(svc);
});

单例组件的注入,可以这样做:

services.AddSingleton<IDataService>(new DataService());

有一个非常有意思的场景,DataService 实现两个接口,如果我们这样做:

验证结果:

我们将会得到两个实例,如果我们想共享一个实例,可以这样做:

验证结果:

如果组件具有依赖项,则可以从服务集合构建服务提供程序并从中获取必要的依赖项:

IServiceProvider provider = services.BuildServiceProvider();

IOtherService otherService = provider.GetRequiredService<IOtherService>();

var dataService = new DataService(otherService);
services.AddSingleton<IDataService>(dataService);
services.AddSingleton<ISomeInterface>(dataService);

但我们一般不会这样使用,也不建议这样使用。

现在我们已经注册了我们的组件,我们可以转向实际使用它们,如下:

  • 构造函数注入

构造函数注入用于在服务构造上声明和获取服务的依赖关系。 例如:

public class ProductService
{
private readonly IProductRepository _productRepository;
public ProductService(IProductRepository productRepository)
{
_productRepository = productRepository;
}
public void Delete(int id)
{
_productRepository.Delete(id);
}
}

ProductService在其构造函数中将IProductRepository注入为依赖项,然后在Delete方法中使用它。

建议的做法:

  • 在构造函数中显示定义所需的依赖项
  • 将注入的依赖项分配给只读【readonly】字段/属性(防止在方法内意外地为其分配另外一个值),如果你的项目接入到sonar就会知道这是一个代码规范。
  • 服务定位器

服务定位器是另外一种获取依赖项的模式,例如:

public class ProductService
{
private readonly IProductRepository _productRepository;
private readonly ILogger<ProductService> _logger;
public ProductService(IServiceProvider serviceProvider)
{
_productRepository = serviceProvider
.GetRequiredService<IProductRepository>();
_logger = serviceProvider
.GetService<ILogger<ProductService>>() ??
NullLogger<ProductService>
.Instance;
}
public void Delete(int id)
{
_productRepository.Delete(id);
_logger.LogInformation($"Deleted a product with id = {id}");
}
}

ProductService 注入了IServiceProvider ,并且使用它获取依赖项。如果你在使用某个依赖项之前没有注入,GetRequiredService 方法将会抛异常,相反GetService 会返回null。

解析构造函数中的服务时,将在释放服务时释放它们,所以,你不用关心释放/处理在构造函数中解析的服务(就像构造函数和属性注入一样)。

建议的做法:

(1)尽可能不使用服务定位器模式,因为该模式存在隐含的依赖关系,这意味着在创建服务实例时无法轻松查看依赖关系,但是该模式对单元测试尤为重要。

(2)如果可能,解析服务构造函数中的依赖项。 解析服务方法会使您的应用程序更加复杂且容易出错。 我将在下一节中介绍问题和解决方案。

再看一个综合的例子:

public class LoggingMiddleware
{
private readonly RequestDelegate _next; public LoggingMiddleware(RequestDelegate next)
{
_next = next;
} public async Task Invoke(HttpContext ctx)
{
Debug.WriteLine("Request starting");
await _next(ctx);
Debug.WriteLine("Request complete");
}
}

在中间件中注入组件有三种不同的方法:

1、构造函数

2、调用参数

3、HttpContext.RequestServices

让我们看看这三种方式注入的使用:

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection; namespace WebAppPerformance
{
// You may need to install the Microsoft.AspNetCore.Http.Abstractions package into your project
public class LoggingMiddleware
{
private readonly RequestDelegate _next;
private readonly IDataService _svc; public LoggingMiddleware(RequestDelegate next, IDataService svc)
{
_next = next;
_svc = svc;
} public async Task Invoke(HttpContext httpContext, IDataService svc2)
{
IDataService svc3 = httpContext.RequestServices.GetService<IDataService>(); Debug.WriteLine("Request starting");
await _next(httpContext);
Debug.WriteLine("Request complete");
}
} // Extension method used to add the middleware to the HTTP request pipeline.
public static class LoggingMiddlewareExtensions
{
public static IApplicationBuilder UseLoggingMiddleware(this IApplicationBuilder builder)
{
return builder.UseMiddleware<LoggingMiddleware>();
}
}
}

中间件在应用程序生命周期中仅实例化一次,因此通过构造函数注入的组件对于所有通过的请求都是相同的。如果IDataService被注册为singleton,我们会在所有这些实例中获得相同的实例。

如果被注册为scoped,svc2并且svc3将是同一个实例,但不同的请求会获得不同的实例;如果在Transient 的情况下,它们都是不同的实例。

注意:我会尽量避免使用RequestServices,只有在中间件中才使用它。

MVC过滤器中注入:

但是,我们不能像往常一样在控制器上添加属性,因为它必须在运行时获得依赖关系。

我们有两个选项可以在控制器或action级别添加它:

[TypeFilter(typeof(TestActionFilter))]
public class HomeController : Controller
{
}
// or
[ServiceFilter(typeof(TestActionFilter))]
public class HomeController : Controller
{
}

关键的区别在于,TypeFilterAttribute将确定过滤器依赖性是什么,通过DI获取它们,并创建过滤器。ServiceFilterAttribute试图从服务集合中找到过滤器!

为了[ServiceFilter(typeof(TestActionFilter))]工作,我们需要更多配置:

public void ConfigureServices(IServiceCollection services)
{
services.AddTransient<TestActionFilter>();
}

现在ServiceFilterAttribute可以找到过滤器了。

如果要全局添加过滤器:

public void ConfigureServices(IServiceCollection services)
{
services.AddMvc(mvc =>
{
mvc.Filters.Add(typeof(TestActionFilter));
});
}

这次不需要将过滤器添加到服务集合中,就像TypeFilterAttribute在每个控制器上添加了一个过滤器一样。

在方法体内解析服务

在某些情况下,您可能需要在方法中解析其他服务。在这种情况下,请确保在使用后释放服务。确保这一点的最佳方法是创建scoped服务,例如:

public class PriceCalculator
{
private readonly IServiceProvider _serviceProvider;
public PriceCalculator(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public float Calculate(Product product, int count,
Type taxStrategyServiceType)
{
using (var scope = _serviceProvider.CreateScope())
{
var taxStrategy = (ITaxStrategy)scope.ServiceProvider
.GetRequiredService(taxStrategyServiceType)
;
var price = product.Price * count;
return price + taxStrategy.CalculateTax(price);
}
}
}

PriceCalculator 在其构造函数中注入IServiceProvider并将其分配给字段。然后,PriceCalculator在Calculate方法中使用它来创建子组件范围。它使用scope.ServiceProvider来解析服务,而不是注入的_serviceProvider实例。因此,从范围中解析的所有服务都将在using语句的末尾自动释放/处理。

建议的做法:

  • 如果要在方法体中解析服务,请始终创建子服务范围以确保正确释放已解析的服务。
  • 如果一个方法把IServiceProvider 作为参数,那么可以直接从中解析出服务,不用关心服务的释放/销毁。创建/管理服务的scoped是调用你方法的代码的责任,所以遵循该原则能是你的代码更简洁。
  • 不要引用已经解析的服务,否则会导致内存泄漏,并且当你后面使用了对象的引用时,将很有机会访问到已经销毁的服务(除非被解析的服务是一个单例)

单例服务

单例服务通常用来保存应用程序的状态,缓存是应用程序状态的一个很好的例子,例如:

public class FileService
{
private readonly ConcurrentDictionary <string,byte []> _cache;
public FileService()
{_
cache = new ConcurrentDictionary <string,byte []>();
}
public byte [] GetFileContent(string filePath)
{
return _cache.GetOrAdd(filePath,_ =>
{
return File.ReadAllBytes(filePath);
});
}
}

FileService只是缓存文件内容以减少磁盘读取。此服务应注册为singleton。否则,缓存将无法按预期工作。

建议的做法:

  • 如果服务保持状态,则应以线程安全的方式访问该状态。因为所有请求同时使用相同的服务实例,所以我使用ConcurrentDictionary而不是Dictionary来确保线程安全。
  • 不要在单例服务中使用scoped和transient 服务,因为transient 服务可能不是线程安全的,如果必须使用它们,那么在使用这些服务时请注意多线程。
  • 内存泄漏通常是单例服务导致的,因为它们将驻留在内存中,直到应用程序结束。所以请确保在合适的时间释放它们,可以参考在方法体内解析服务部分。
  • 如果缓存数据(本示例中的文件内容),则应创建一种机制,以便在原始数据源更改时更新/使缓存的数据无效(当此示例中磁盘上的缓存文件发生更改时)。

域服务

Scoped生命周期首先似乎是存储每个Web请求数据的良好候选者。 因为ASP.NET Core会为每个Web请求创建一个服务范围【同一个http请求会在同一个域内】。 因此,如果您将服务注册为Scoped,则可以在Web请求期间共享该服务。 例:

public class RequestItemsService
{
private readonly Dictionary<string, object> _items;
public RequestItemsService()
{
_items = new Dictionary<string, object>();
}
public void Set(string name, object value)
{
_items[name] = value;
}
public object Get(string name)
{
return _items[name];
}
}

如果你以scoped注入RequestItemsService 并将其注入到两个不同的服务中去,那么你可以从另外一个服务中获取添加的项,因为它们将共享相同的RequestItemsService实例,这也是我们所期望看到的。但是事实并不是我们想象的那样。如果你创建一个子域,并从子域中获取RequestItemsService ,那么你将会获取一个新的RequestItemsService 实例,并且这个新的实例并不会像你期望的那样工作。所以,scoped服务并不总是表示每个Web请求的实例。你可能认为自己不会出现这样的错误,但是,你并不能保证别人不会创建子域,并从中解析服务。

建议的做法:

  • 一个scoped服务可以被认为是一个Web请求中太多服务被注入的优化。因此在相同的web请求期间,所有这些服务将会使用一个实例。
  • scoped服务不需要设计为线程安全的,因为,它们通常应有单个web请求/线程使用。但是!你不应该在不同的线程之间共享scope服务!
  • 如果您设计scoped服务以在Web请求中的其他服务之间共享数据,请务必小心!!!您可以将每个Web请求数据存储在HttpContext中(注入IHttpContextAccessor以访问它),这是更安全的方式。HttpContext的生命周期不是作用域。实际上,它根本没有注册到DI(这就是为什么你不注入它,而是注入IHttpContextAccessor)。HttpContextAccessor实现使用AsyncLocal在Web请求期间共享相同的HttpContext。

三、总结:

依赖注入起初看起来很简单,但是如果你不遵循一些严格的原则,就会存在潜在的多线程和内存泄漏问题。如果有理解和翻译不对的地方,还请指出来。到底服务以哪种方式注册,还是要看具体的场景和业务需求,上面是一些建议,能遵守上面的建议,会避免一些不必要的问题。可能有些地方理解的还不是很深刻,只要在编码时有这种意识就非常好了,这也是我写这篇博客的原因。好了,就聊到这里,后面还会探讨ASP.Net Core MVC配置相关的源码,依赖注入是.Net Core中的核心,如果对依赖注入基础知识还不太明白的话,可以参考老A和腾飞两位大佬的博客:

https://www.cnblogs.com/artech/p/dependency-injection-in-asp-net-core.html

https://www.cnblogs.com/jesse2013/p/di-in-aspnetcore.html

参考文章:

https://medium.com/volosoft/asp-net-core-dependency-injection-best-practices-tips-tricks-c6e9c67f9d96

https://joonasw.net/view/aspnet-core-di-deep-dive

作者:郭峥

出处:http://www.cnblogs.com/runningsmallguo/

本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文链接。

ASP.NET Core依赖注入——依赖注入最佳实践的更多相关文章

  1. ASP.NET Core中的依赖注入(4): 构造函数的选择与服务生命周期管理

    ServiceProvider最终提供的服务实例都是根据对应的ServiceDescriptor创建的,对于一个具体的ServiceDescriptor对象来说,如果它的ImplementationI ...

  2. ASP.NET Core中的依赖注入(5): ServiceProvider实现揭秘 【解读ServiceCallSite 】

    通过上一篇的介绍我们应该对实现在ServiceProvider的总体设计有了一个大致的了解,但是我们刻意回避一个重要的话题,即服务实例最终究竟是采用何种方式提供出来的.ServiceProvider最 ...

  3. ASP.NET Core中的依赖注入(1):控制反转(IoC)

    ASP.NET Core在启动以及后续针对每个请求的处理过程中的各个环节都需要相应的组件提供相应的服务,为了方便对这些组件进行定制,ASP.NET通过定义接口的方式对它们进行了"标准化&qu ...

  4. ASP.NET Core中的依赖注入(2):依赖注入(DI)

    IoC主要体现了这样一种设计思想:通过将一组通用流程的控制从应用转移到框架之中以实现对流程的复用,同时采用"好莱坞原则"是应用程序以被动的方式实现对流程的定制.我们可以采用若干设计 ...

  5. ASP.NET Core中的依赖注入(3): 服务的注册与提供

    在采用了依赖注入的应用中,我们总是直接利用DI容器直接获取所需的服务实例,换句话说,DI容器起到了一个服务提供者的角色,它能够根据我们提供的服务描述信息提供一个可用的服务对象.ASP.NET Core ...

  6. ASP.NET Core中的依赖注入(5): ServiceProvider实现揭秘 【总体设计 】

    本系列前面的文章我们主要以编程的角度对ASP.NET Core的依赖注入系统进行了详细的介绍,如果读者朋友们对这些内容具有深刻的理解,我相信你们已经可以正确是使用这些与依赖注入相关的API了.如果你还 ...

  7. ASP.NET Core中的依赖注入(5):ServicePrvider实现揭秘【补充漏掉的细节】

    到目前为止,我们定义的ServiceProvider已经实现了基本的服务提供和回收功能,但是依然漏掉了一些必需的细节特性.这些特性包括如何针对IServiceProvider接口提供一个Service ...

  8. ASP.NET Core 中的依赖注入

    目录 什么是依赖注入 ASP .NET Core 中使用依赖注入 注册 使用 释放 替换为其它的 Ioc 容器 参考 什么是依赖注入 软件设计原则中有一个依赖倒置原则(DIP),为了更好的解耦,讲究要 ...

  9. ASP.NET Core 中的 依赖注入介绍

    ASP.NET Core 依赖注入 HomeController public class HomeController : Controller { private IStudentReposito ...

  10. C# 嵌入dll 动软代码生成器基础使用 系统缓存全解析 .NET开发中的事务处理大比拼 C#之数据类型学习 【基于EF Core的Code First模式的DotNetCore快速开发框架】完成对DB First代码生成的支持 基于EF Core的Code First模式的DotNetCore快速开发框架 【懒人有道】在asp.net core中实现程序集注入

    C# 嵌入dll   在很多时候我们在生成C#exe文件时,如果在工程里调用了dll文件时,那么如果不加以处理的话在生成的exe文件运行时需要连同这个dll一起转移,相比于一个单独干净的exe,这种形 ...

随机推荐

  1. Linux重命名网卡名称

    1.查看当前网卡: nmcli connection show 可以看到我有两个网卡,其中一个为中文名称,我想将配置 2 修改为net-DHCP 2.cd到/etc/sysconfig/network ...

  2. openstack nova工作流程

    工作流程请求:nova boot --image ttylinux --flavor 1 i-01nova-api 接受请求,一个tcp REST请求.nova-api 发送一个创建虚拟机的请求到消息 ...

  3. 洗礼灵魂,修炼python(91)-- 知识拾遗篇 —— pymysql模块之python操作mysql增删改查

    首先你得学会基本的mysql操作语句:mysql学习 其次,python要想操作mysql,靠python的内置模块是不行的,而如果通过os模块调用cmd命令虽然原理上是可以的,但是还是不太方便,那么 ...

  4. IIS 使用OpenSSL 生成的自签名证书,然后使用SingalR 客户端访问Https 站点通信

    使用SignalR 的客户端去发送消息给使用 https 部署的站点,官方文档目前并没有详细的教程,所以在此记录下步骤: 使用管理员身份打开cmd 窗口,选择一个整数保存文件夹的地址,切换到对应的文件 ...

  5. 通过http上下文判断是否是Ajax请求

    using System; namespace System.Web.Mvc { /// <summary>Represents a class that extends the < ...

  6. Java中的生产消费者问题

    package day190109; import java.util.LinkedList; import java.util.Queue; import java.util.Random; pub ...

  7. c/c++链队列

    链队列 链队列就是简化了的单链表 nodequeue.h #ifndef __NODEQUEUE__ #define __NODEQUEUE__ #include <stdio.h> #i ...

  8. LeetCode算法题-Number of 1 Bits(Java实现)

    这是悦乐书的第186次更新,第188篇原创 01 看题和准备 今天介绍的是LeetCode算法题中Easy级别的第45题(顺位题号是191).编写一个带无符号整数的函数,并返回它所具有的"1 ...

  9. mongoDB2.6,java使用具体解释

    本文使用的环境是:win7_64+Eclipse+maven 一.准备工作 下载java驱动包 驱动包下载地址:http://mvnrepository.com/artifact/org.mongod ...

  10. http和https工具类 (要注意httpclient版本号和log4j的版本号)

    1 工具类 package dd.com; import java.io.IOException; import java.security.cert.CertificateException; im ...