ASP.NET Core支持依赖关系注入(DI)软件设计模式,这是一种在类及其依赖关系之间实现控制反转(IOC)的技术。

依赖关系注入概述

  依赖项是指另一个对象所依赖的对象。

1
2
3
4
5
6
7
8
9
10
public class IndexModel : PageModel
{
// 直接实例化MyDependency类(直接依赖)
private readonly MyDependency _dependency = new MyDependency();

public void OnGet()
{
_dependency.WriteMessage("IndexModel.OnGet");
}
}

  在上述示例中,创建并直接依赖于 MyDependency 类。 代码依赖项会产生问题,应避免使用,原因如下:

  • 要用不同的实现替换 MyDependency,必须修改 IndexModel 类。
  • 如果 MyDependency 具有依赖项,则必须由 IndexModel 类对其进行配置。 在具有多个依赖于 MyDependency 的类的大型项目中,配置代码将分散在整个应用中。
  • 这种实现很难进行单元测试。

  依赖关系注入通过以下方式解决了这些问题:

  • 使用接口或基类将依赖关系实现抽象化。
  • 在服务容器中注册依赖关系。 ASP.NET Core 提供了一个内置的服务容器 IServiceProvider。 服务通常已在应用的 Program.cs 文件中注册。
  • 将服务注入到使用它的类的构造函数中。 框架负责创建依赖关系的实例,并在不再需要时将其释放。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Index2Model : PageModel
{
private readonly IMyDependency _myDependency;

// 首先依赖接口,其次通过构造函数注入
public Index2Model(IMyDependency myDependency)
{
_myDependency = myDependency;
}

public void OnGet()
{
_myDependency.WriteMessage("Index2Model.OnGet");
}
}
1
builder.Service.AddTransient<IMyDependency,MyDependency>();

  以上示例中,通过依赖注入的模式:

  • 不使用具体类型 MyDependency,仅使用它实现的 IMyDependency 接口。 这样可以轻松地更改实现,而无需修改控制器或 Razor 页面。
  • 不创建 MyDependency 的实例,这由 DI 容器创建。

使用扩展方法注册服务组

  ASP.NET Core 框架使用一种约定来注册一组相关服务。 约定使用单个 Add{GROUP_NAME} 扩展方法来注册该框架功能所需的所有服务。 例如,AddControllers 扩展方法会注册 MVC 控制器所需的服务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public static class MyConfigServiceCollectionExtensions
{
public static IServiceCollection AddConfig(
this IServiceCollection services, IConfiguration config)
{
services.Configure<PositionOptions>(
config.GetSection(PositionOptions.Position));
services.Configure<ColorOptions>(
config.GetSection(ColorOptions.Color));

return services;
}

public static IServiceCollection AddMyDependencyGroup(
this IServiceCollection services)
{
services.AddScoped<IMyDependency, MyDependency>();
services.AddScoped<IMyDependency2, MyDependency2>();

return services;
}
}
1
2
3
builder.Services
.AddConfig(builder.Configuration)
.AddMyDependencyGroup();

服务生存期

  要在中间件中使用范围内服务,请使用以下方法之一:

  • 将服务注入中间件的 InvokeInvokeAsync 方法。 使用构造函数注入会引发运行时异常,因为它强制使范围内服务的行为与单一实例类似。 生存期和注册选项部分中的示例演示了 InvokeAsync 方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class MyCustomMiddleware
{
private readonly RequestDelegate _next;

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

// IMessageWriter is injected into InvokeAsync
public async Task InvokeAsync(HttpContext httpContext, IMessageWriter svc)
{
svc.Write(DateTime.Now.Ticks.ToString());
await _next(httpContext);
}
}
  • 使用基于工厂的中间件。 使用此方法注册的中间件按客户端请求(连接)激活,这也使范围内服务可注入中间件的构造函数。
1
2
3
4
5
6
7
8
9
10
public class CustomMiddleware : IMiddleware
{
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
Console.WriteLine("Before invoking the next middleware");
// 调用下一个中间件
await next(context);
Console.WriteLine("After invoking the next middleware");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class CustomMiddlewareFactory : IMiddlewareFactory
{
private readonly IServiceProvider _serviceProvider;

public CustomMiddlewareFactory(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}

public IMiddleware Create(Type middlewareType)
{
// 使用服务提供者创建中间件实例
return (IMiddleware)_serviceProvider.GetService(middlewareType);
}

public void Release(IMiddleware middleware)
{
// 释放中间件,如果需要自定义逻辑可以在这里处理
(middleware as IDisposable)?.Dispose();
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var builder = WebApplication.CreateBuilder(args);

// 注册自定义中间件
builder.Services.AddTransient<CustomMiddleware>();

// 注册自定义的 IMiddlewareFactory 实现
builder.Services.AddSingleton<IMiddlewareFactory, CustomMiddlewareFactory>();

var app = builder.Build();

// 使用自定义中间件
app.UseMiddleware<CustomMiddleware>();

app.Run();

服务注册方法

在为测试模拟类型时,使用多个实现很常见。

  仅使用实现类型注册服务等效于使用相同的实现和服务类型注册该服务。 因此,我们不能使用捕获显式服务类型的方法来注册服务的多个实现。 这些方法可以注册服务的多个实例,但它们都具有相同的实现类型 。

  上述任何服务注册方法都可用于注册同一服务类型的多个服务实例。 下面的示例以 IMyDependency 作为服务类型调用 AddSingleton 两次。 第二次对 AddSingleton 的调用在解析为 IMyDependency 时替代上一次调用,在通过 IEnumerable 解析多个服务时添加到上一次调用。 通过 IEnumerable<{SERVICE}> 解析服务时,服务按其注册顺序显示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
services.AddSingleton<IMyDependency, MyDependency>();
services.AddSingleton<IMyDependency, DifferentDependency>();

public class MyService
{
public MyService(IMyDependency myDependency,
IEnumerable<IMyDependency> myDependencies)
{
Trace.Assert(myDependency is DifferentDependency);

var dependencyArray = myDependencies.ToArray();
Trace.Assert(dependencyArray[0] is MyDependency);
Trace.Assert(dependencyArray[1] is DifferentDependency);
}
}

密钥服务

  密钥服务是指使用密钥注册和检索依赖项注入 (DI) 服务的机制。 通过调用 AddKeyedSingleton (或 AddKeyedScopedAddKeyedTransient)来注册服务,与密钥相关联。 使用 [FromKeyedServices] 属性指定密钥来访问已注册的服务。 以下代码演示如何使用键化服务:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.SignalR;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddKeyedSingleton<ICache, BigCache>("big");
builder.Services.AddKeyedSingleton<ICache, SmallCache>("small");
builder.Services.AddControllers();

var app = builder.Build();

app.MapGet("/big", ([FromKeyedServices("big")] ICache bigCache) => bigCache.Get("date"));
app.MapGet("/small", ([FromKeyedServices("small")] ICache smallCache) =>
smallCache.Get("date"));

app.MapControllers();

app.Run();

public interface ICache
{
object Get(string key);
}
public class BigCache : ICache
{
public object Get(string key) => $"Resolving {key} from big cache.";
}

public class SmallCache : ICache
{
public object Get(string key) => $"Resolving {key} from small cache.";
}

[ApiController]
[Route("/cache")]
public class CustomServicesApiController : Controller
{
[HttpGet("big-cache")]
public ActionResult<object> GetOk([FromKeyedServices("big")] ICache cache)
{
return cache.Get("data-mvc");
}
}

public class MyHub : Hub
{
public void Method([FromKeyedServices("small")] ICache cache)
{
Console.WriteLine(cache.Get("signalr"));
}
}

在应用启动时解析服务

  以下代码显示如何在应用启动时限时解析范围内服务:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddScoped<IMyDependency, MyDependency>();

var app = builder.Build();

using (var serviceScope = app.Services.CreateScope())
{
var services = serviceScope.ServiceProvider;

var myDependency = services.GetRequiredService<IMyDependency>();
myDependency.WriteMessage("Call services from main");
}

app.MapGet("/", () => "Hello World!");

app.Run();

设计能够进行依赖关系注入的服务

  在设计能够进行依赖注入的服务时:

  • 避免有状态的、静态类和成员。 通过将应用设计为改用单一实例服务,避免创建全局状态。
  • 避免在服务中直接实例化依赖类。 直接实例化会将代码耦合到特定实现。
  • 不在服务中包含过多内容,确保设计规范,并易于测试。

默认服务容器替换

  内置的服务容器旨在满足框架和大多数消费者应用的需求。 建议使用内置容器,除非你需要的特定功能不受它支持,例如:

  • 属性注入
  • 基于名称的注入(仅限 .NET 7 和更早版本。有关详细信息,请参阅密钥服务。)
  • 子容器
  • 自定义生存期管理
  • 对迟缓初始化的 Func 支持
  • 基于约定的注册

  以下第三方容器可用于 ASP.NET Core 应用:

框架提供的服务

  Program.cs 注册应用使用的服务,包括 Entity Framework CoreASP.NET Core MVC 等平台功能。 最初,提供给 Program.csIServiceCollection 具有框架定义的服务(具体取决于IServiceCollection)。 对于基于 ASP.NET Core 模板的应用,该框架会注册 250 个以上的服务。

  下表列出了框架注册的这些服务的一小部分:

服务类型 生存期
Microsoft.AspNetCore.Hosting.Builder.IApplicationBuilderFactory 暂时
IHostApplicationLifetime 单例
IWebHostEnvironment 单例
Microsoft.AspNetCore.Hosting.IStartup 单例
Microsoft.AspNetCore.Hosting.IStartupFilter 暂时
Microsoft.AspNetCore.Hosting.Server.IServer 单例
Microsoft.AspNetCore.Http.IHttpContextFactory 暂时
Microsoft.Extensions.Logging.ILogger 单例
Microsoft.Extensions.Logging.ILoggerFactory 单例
Microsoft.Extensions.ObjectPool.ObjectPoolProvider 单例
Microsoft.Extensions.Options.IConfigureOptions 暂时
Microsoft.Extensions.Options.IOptions 单例
System.Diagnostics.DiagnosticSource 单例
System.Diagnostics.DiagnosticListener 单例