依赖注入 (Dependency injection,DI) 是一种实现对象及其合作者或依赖项之间松散耦合的技术。将用来执行操作 (Action) 的对象以某种方式提供给该类,而不是直接实例化合作者或使用静态引用。通常,Class 会通过它们的构造函数声明其依赖关系,允许它们遵循 显示依赖原则 (Explicit Dependencies Principle) ,这种方法被称为 “构造函数注入(constructor injection)”。

Class 的设计中使用 DI 思想会使它们的依赖关系变得更加耦合,因为它们没有对它们的合作者直接硬编码的依赖,他们遵循 依赖倒置原则(Dependency Inversion Principle)中 “高层模块不应该依赖于低层模块,两者都应该依赖于抽象。”的设计思路, 要求 Class 在它们构造时向其提供抽象(通常是 interfaces ),而不是直接引用特定的实现。提取接口的依赖关系和提供这些接口的实现作为参数也是 策略设计模式(Strategy design pattern) 的一个示例。

依赖注入原理

// 约定一个抽象接口
public interface IRepository
{
    string GetInfo();
}

// 使用EF实现接口
public class EFRepository : IRepository
{
    public string GetInfo()
    {
        return "load data form ef!";
    }
}

// 使用Dapper实现接口
public class DapperRepository : IRepository
{
    public string GetInfo()
    {
        return "load data form dapper!";
    }
}

// 在用来执行操作的 Service 的构造函数中提供抽象接口
public class OperationService
{
    private readonly IRepository _repository;
    public OperationService(IRepository repository)
    {
        _repository = repository;
    }

    public string GetList()
    {
        return _repository.GetInfo();
    }
}   

通过以上代码可以看出,Service 与 Repository 之间没有强依赖关系,而是都与抽象接口 IRepository 建立关系。

OperationService service = new OperationService(new EFRepository());

// Or

OperationService service = new OperationService(new DapperRepository());

service.GetInfo();

在调用过程中,由调用方来决定使用哪种实现方式,这就实现了依赖倒置原则。

当系统被设计使用 DI 时,将会有很多 Class 通过它们的构造函数(或属性)请求其依赖关系,这是我们需要有一个公共函数被用来帮助我们创建这些 Class 及其相关的依赖关系。这个公共函数被称为 容器(containers) ,或是更具体地称为控制反转(Inversion of Control,IoC) 容器或者依赖注入(Dependency injection,DI)容器。容器本质上是一个工厂,负责提供向它请求的类型实例。如果一个给定类型声明它具有依赖关系,并且容器已经被配置为提供依赖类型,它将把创建依赖关系作为创建请求实例的一部分。通过这种方式,可以向类型提供复杂的依赖关系而不需要任何硬编码的类型构造。除了创建对象的依赖关系,容器通常还会管理应用程序中对象的生命周期。

ASP.NET Core 中的依赖注入容器

ASP.NET Core 已将 DI 作为基础设施,并提供了一个默认支持构造函数注入的简单内置容器(由 IServiceProvider 接口表示),使 ASP.NET Core 的某些服务可以通过 DI 获取。ASP.NET 的容器指的是它管理的类型为 services,services 是指由 ASP.NET Core 的 IoC 容器管理的类型。可以在应用程序 Startup 类的 ConfigureServices 方法中配置内置容器的服务。

public void ConfigureServices(IServiceCollection services)
{
    services.AddTransient<IRepository, EFRepository>();
    services.AddTransient<IRepository, DapperRepository>();

    services.AddTransient<OperationService, OperationService>();
}

ASP.NET Core 中的构造函数注入行为要求被注入构造函数是公开的 (public),而且被注入的构造函数支持重载。

以上示例中,相同的接口,注册不同的实现类会按照注册顺序进行覆盖,最终有效的实现是最后注册的类型。

定义一个中间件来测试依赖注入的调用:

public class RepositoryMiddleware
{
    private readonly RequestDelegate _next;

    public RepositoryMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task Invoke(HttpContext context,
        OperationService operationService)
    {
        await context.Response.WriteAsync(operationService.GetList());
    }
}

public static class RepositoryMiddlewareExtensions
{
    public static IApplicationBuilder UseRepositoryMiddleware(this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<RepositoryMiddleware>();
    }
}
// Startup.cs
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    app.UseRepositoryMiddleware();
}

服务生命周期和注册选项

ASP.NET Core 服务可以被配置为以下生命周期:

Transient

瞬时:服务在它们每次被请求时都会被创建。

Scoped

作用域:服务在每次请求只会被创建一次。

Singleton

单例:生命周期服务在它们第一次被请求时创建(或者如果你在 ConfigureServices 运行时指定一个实例),之后每个后续请求将使用第一次被创建的实例。

public interface IOperation
{
    Guid OperationId { get; }
}

public interface IOperationTransient : IOperation
{
}
public interface IOperationScoped : IOperation
{
}
public interface IOperationSingleton : IOperation
{
}
public interface IOperationSingletonInstance : IOperation
{
}

public class Operation : IOperationTransient,
    IOperationScoped,
    IOperationSingleton,
    IOperationSingletonInstance
{
    Guid _OperationId;

    public Guid OperationId => _OperationId;

    public Operation()
    {
        _OperationId = Guid.NewGuid();
    }

    public Operation(Guid operationId)
    {
        _OperationId = operationId;
    }
}

public class OperationService2
{
    public IOperationTransient TransientOperation { get; }
    public IOperationScoped ScopedOperation { get; }
    public IOperationSingleton SingletonOperation { get; }
    public IOperationSingletonInstance SingletonInstanceOperation { get; }

    public OperationService2(IOperationTransient transientOperation,
        IOperationScoped scopedOperation,
        IOperationSingleton singletonOperation,
        IOperationSingletonInstance instanceOperation)
    {
        TransientOperation = transientOperation;
        ScopedOperation = scopedOperation;
        SingletonOperation = singletonOperation;
        SingletonInstanceOperation = instanceOperation;
    }
}

以上示例中使用 Operation 类实现这些接口。它的构造函数接收一个 Guid,若未提供则生成一个新的 Guid。

接下来,在 ConfigureServices 中将每一个类型根据它们命名的生命周期被添加到容器中:

services.AddTransient<IOperationTransient, Operation>();
services.AddScoped<IOperationScoped, Operation>();
services.AddSingleton<IOperationSingleton, Operation>();
services.AddSingleton<IOperationSingletonInstance>(new Operation(Guid.Empty));
services.AddTransient<OperationService2, OperationService2>();

再次定义一个中间件来测试依赖注入的调用:

public class ServiceLifetimesMiddleware
{
    private readonly RequestDelegate _next;

    public ServiceLifetimesMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task Invoke(HttpContext context,
        OperationService2 operationService2,
        IOperationTransient transientOperation,
        IOperationScoped scopedOperation,
        IOperationSingleton singletonOperation,
        IOperationSingletonInstance singletonInstanceOperation)
    {
        await context.Response.WriteAsync($"Operations:\r\n");
        await context.Response.WriteAsync($"Transient:{transientOperation.OperationId}\r\n");
        await context.Response.WriteAsync($"Scoped:{scopedOperation.OperationId}\r\n");
        await context.Response.WriteAsync($"Singleton:{singletonOperation.OperationId}\r\n");
        await context.Response.WriteAsync($"SingletonInstance:{singletonInstanceOperation.OperationId}\r\n");
        await context.Response.WriteAsync("\r\n");
        await context.Response.WriteAsync($"Serivce Operations:\r\n");
        await context.Response.WriteAsync($"Transient:{operationService2.TransientOperation.OperationId}\r\n");
        await context.Response.WriteAsync($"Scoped:{operationService2.ScopedOperation.OperationId}\r\n");
        await context.Response.WriteAsync($"Singleton:{operationService2.SingletonOperation.OperationId}\r\n");
        await context.Response.WriteAsync($"SingletonInstance:{operationService2.SingletonInstanceOperation.OperationId}\r\n");
    }
}

public static class ServiceLifetimesMiddlewareExtensions
{
    public static IApplicationBuilder UseServiceLifetimesMiddleware(this IApplicationBuilder builder)
    {
        return builder.UseMiddleware<ServiceLifetimesMiddleware>();
    }
}
// Startup.cs
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    app.UseServiceLifetimesMiddleware();
}

通过两次独立的请求来测试效果:

Operations:
Transient:bc337e93-0813-4d79-85b0-084f0d91a0cf
Scoped:0ff39177-42aa-4646-a1d4-5238a8bfb5b7
Singleton:72b37cae-edc3-411a-9e94-97ec29fdcd6a
SingletonInstance:00000000-0000-0000-0000-000000000000

Serivce Operations:
Transient:4efe931a-8004-4900-851f-8253f81d6ae3
Scoped:0ff39177-42aa-4646-a1d4-5238a8bfb5b7
Singleton:72b37cae-edc3-411a-9e94-97ec29fdcd6a
SingletonInstance:00000000-0000-0000-0000-000000000000
Operations:
Transient:57ac7f0a-e23e-460a-9435-55501a88f3b6
Scoped:e73cffd6-ff54-44df-a1e2-e4753d3a7ea5
Singleton:72b37cae-edc3-411a-9e94-97ec29fdcd6a
SingletonInstance:00000000-0000-0000-0000-000000000000

Serivce Operations:
Transient:748017c1-9a18-465b-b23c-75abdc910fd1
Scoped:e73cffd6-ff54-44df-a1e2-e4753d3a7ea5
Singleton:72b37cae-edc3-411a-9e94-97ec29fdcd6a
SingletonInstance:00000000-0000-0000-0000-000000000000

通过 OperationId 值在两次请求中变化,得知:

  • 瞬时(Transient) 对象总是不同的,框架向每一个控制器和每一个服务提供了一个新的实例
  • 作用域(Scoped) 对象在一次请求中是相同的,但在不同请求中是不同的
  • 单例(Singleton) 当对象包含在任意一个对象内和每个请求中都相同的(无论是否在 ConfigureServices 中提供实例)

Request Services

在一个ASP的可用服务。净的请求 HttpContext暴露在 RequestServices收集。

请求服务代表服务配置和请求作为应用程序的一部分。当你的对象指定依赖关系,这些满足要求的对象通过查找 RequestServices 中对应的类型得到,而不是 ApplicationServices

通常,我们不应该直接使用这些属性,而更倾向于通过类的构造函数请求需要的服务类型,并且使用框架来注入依赖关系。这将会生成更易于测试的 (查看 Testing) 和更松散耦合的类。

依赖注入服务的设计思路

在应用程序中,我们通过设计依赖注入服务来获取它们的合作者。这意味着在编码过程中避免使用有状态的静态方法和直接实例化依赖的类型。面临选择实例化一个类型还是通过依赖注入请求它时,New is Glue 这句话可以帮助你。通过遵循 面向对象设计的 SOLID 原则,你的类将倾向于小、易于分解及易于测试。

如果我们发现设计的类中往往会有太多的依赖关系被注入时该怎么办?这通常表明这个类试图做太多的事情,并且可能违反了单一职责原则(SRP)。我们是否可以通过转移一些职责到一个新的类来重构类。请记住,Controller 类应该重点关注用户界面(User Interface,UI),因此业务规则和数据访问实现细节应该保存在这些适合单独关注的类中。

关于数据访问,如果已经在 Startup 类中配置了 EF,那么我们能够方便的注入 Entity Framework 的 DbContext 类型到控制器中。然而,最好不要在 UI 项目直接依赖 DbContext。相反,依赖于一个抽象(比如一个仓储接口),并且限定使用 EF (或其他任何数据访问技术)来实现这个接口。这将减少应用程序和特定的数据访问策略之间的耦合,并且使应用程序代码更容易测试。

全部评论

发一条友善的评论... 取消回复

提醒 : 您所填写的邮箱地址不会被公开!

看不清?点击更换