ASP.NET Core依赖注入——依赖注入最佳实践

在这篇文章中,我们将深入研究.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/

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

原文地址:https://www.cnblogs.com/runningsmallguo/p/10234307.html

时间: 2024-10-28 15:14:14

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

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

通过上一篇的介绍我们应该对实现在ServiceProvider的总体设计有了一个大致的了解,但是我们刻意回避一个重要的话题,即服务实例最终究竟是采用何种方式提供出来的.ServiceProvider最终采用何种方式提供我们所需的服务实例取决于最终选择了怎样的ServiceCallSite,而服务注册是采用的ServiceDescriptor有决定了ServiceCallSite类型的选择.我们将众多不同类型的ServiceCallSite大体分成两组,一组用来创建最终的服务实例,另一类则与生命周

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

参考页面: http://www.yuanjiaocheng.net/ASPNET-CORE/project-layout.html http://www.yuanjiaocheng.net/ASPNET-CORE/projectjson.html http://www.yuanjiaocheng.net/ASPNET-CORE/core-configuration.html http://www.yuanjiaocheng.net/ASPNET-CORE/core-middleware.htm

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

到目前为止,我们定义的ServiceProvider已经实现了基本的服务提供和回收功能,但是依然漏掉了一些必需的细节特性.这些特性包括如何针对IServiceProvider接口提供一个ServiceProvider对象,何创建ServiceScope,以及如何提供一个服务实例的集合. 一.提供一个ServiceProvider对象 我们知道当将服务类型指定为IServiceProvider接口并调用ServiceProvider的GetService方法是,ServiceProvider对象本

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

本系列前面的文章我们主要以编程的角度对ASP.NET Core的依赖注入系统进行了详细的介绍,如果读者朋友们对这些内容具有深刻的理解,我相信你们已经可以正确是使用这些与依赖注入相关的API了.如果你还对这个依赖注入系统底层的实现原理具有好奇心,可以继续阅读这一节的内容. 目录一.ServiceCallSite 二.Service 三.ServiceEntry 四.ServiceTable 五.ServiceProvider 作为DI容器的体现,ServiceProvider是ASP.NET Co

ASP.NET Core DI 手动获取注入对象

原文:ASP.NET Core DI 手动获取注入对象 ASP.NET Core DI 一般使用构造函数注入获取对象,比如在ConfigureServices配置注入后,通过下面方式获取: private IValueService _valueService; public ValueController(IValueService valueService) { _valueService = valueService; } 那如果手动获取注入对象呢? 第一种获取方式(有时会获取不到,不推荐

ASP.NET Core DI 手动获取注入对象 (转)

ASP.NET Core DI 手动获取注入对象:https://www.cnblogs.com/xishuai/p/asp-net-core-ioc-di-get-service.html 方法1(获取Transient和Singleton注入的对象):      在 Startup 类的 Configure 方法中,获取 DI 容器,然后保留在一个静态类的静态属性中.      补充:需要注意的是,使用ServiceLocator.Instance.GetService<T>();,只能获

ASP.NET Core 中的依赖注入 [共7篇]

一.控制反转(IoC) ASP.NET Core在启动以及后续针对每个请求的处理过程中的各个环节都需要相应的组件提供相应的服务,为了方便对这些组件进行定制,ASP.NET通过定义接口的方式对它们进行了“标准化”,我们将这些标准化的组件称为服务,ASP.NET在内部专门维护了一个DI容器来提供所需的服务.要了解这个DI容器以及现实其中的服务提供机制,我们先得知道什么是DI(Dependence Injection),而一旦我们提到DI,又不得不说IoC(Inverse of Control)… [

asp.net core 系列 3 依赖注入

一. 依赖注入概述 在软件设计的通用原则中,SOLID是非常流行的缩略语,它由5个设计原则的首字母构成:单一原则(S).开放封闭原则(O).里氏替换原则(L).接口分离原则(I).依赖反转原则(D).本篇介绍依赖反转原则以及在ASP.NET Core中的实现. 直接依赖是指:当一个类需要另一个类协作来完成工作的时候就产生了依赖.举例比如:模块 A 调用模块 B 中的函数,而模块 B 又调用模块 C 中的函数,则编译时 A 取决于 B,而 B 又取决于 C.这是有严重的依赖关系,不属于松散耦合.

ASP.NET Core MVC 之依赖注入 Controller

ASP.NET Core MVC 控制器应通过构造函数明确地请求它们地依赖关系,在某些情况下,单个控制器地操作可能需要一个服务,在控制器级别上的请求可能没有意义.在这种情况下,也可以将服务作为  Action 的参数. 依赖注入是一种如 Dependency Inversion Principle 所示的技术,允许应用程序松散耦合的模块组成. 1.构造函数注入 ASP.NET Core 内置的基于构造函数的依赖注入支持扩展到 MVC 控制器.通过只添加一个服务类型作为构造函数参数到控制器中,AS

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

ASP.NET Core在启动以及后续针对每个请求的处理过程中的各个环节都需要相应的组件提供相应的服务,为了方便对这些组件进行定制,ASP.NET通过定义接口的方式对它们进行了"标准化",我们将这些标准化的组件称为服务,ASP.NET在内部专门维护了一个DI容器来提供所需的服务.要了解这个DI容器以及现实其中的服务提供机制,我们先得知道什么是DI(Dependence Injection),而一旦我们提到DI,又不得不说IoC(Inverse of Control). 目录 一.流程控