依赖注入是一种设计模式,在类及其依赖关系之间实现控制反转(IOC)的技术。如果想要彻底理解依赖注入的设计模式,则先要清楚如下几个概念:
- 依赖倒置原则,简称DIP
- 控制反转,简称IOC
- 依赖注入,简称DI
- 控制反转容器,简称IOC容器
Blazor web App项目中已经将依赖注入的设计模式加入其中了,因此在整个框架中,依赖注入无处不在。对于框架本身,内置了许多的组件服务供开发者使用,例如配置组件、日志组件、选项组件等,也可以将自己定义的服务进行注册。
Blazor 项目中,服务的注册默认是在Program
中进行的。
服务的注册与注入
一、服务的生存期限
在Blazor Web App项目中,服务的注册是通过IServiceCollection
接口来进行的,该接口主要提供了三种类型的注册方法,即AddScoped
、AddSingleton
以及AddTransient
,三种方法分别对应了服务的三种生命周期。
Scoped
Scoped
:发生请求时创建一个新的服务实例,仅在此请求内有效,所以也叫作用域服务。
需要注意的是,Blazor的客户端(Blazor WebAssembly)是没有作用域的概念的,注册为Scoped
的服务的行为与Singleton
是一样的。在客户端上的组件间导航时,作用域服务不会重建,因为其中与服务器之间的通信是通过用户线路的 SignalR 连接进行,而不是通过 HTTP 请求进行的。因此客户端只有在以下情况发生时,才会重建作用域服务:
- 用户关闭了浏览器窗口,打开了一个新窗口,并向后导航到该应用。
- 用户在浏览器窗口中关闭应用的一个选项卡,然后打开了一个新的选项卡,并向后导航到该应用。
- 用户选择浏览器的重新加载/刷新按钮。
Singleton
Singleton
:单一实例服务,项目中从IOC容器中获取到的该服务,都是相同的实例。
Transient
Transient
:每当组件从服务容器获取 Transient
服务的实例时,都会接收到一个该服务的新实例。
二、注册服务
Blazor中的服务注册跟其他IOC容器差不多的,在Program
中通过使用builder.Services
对象(IServiceCollection
)进行服务的注册。
-
Blazor中的依赖注入使用的是
Microsoft.Extensions.DependencyInjection
,具体用法就不在这里展开了。 -
对于新服务的注册,服务端(Blazor Server)和客户端(Blazor WebAssembly)是一样的。
-
Program.cs
...... builder.Services.AddSingleton<IMyService, MyService>(); ......
在Blazor Web App Auto项目中,如果一个使用客户端渲染且支持预渲染的组件,想要通过注入的方式使用IOC容器中的服务,那么这个服务必须在服务端和客户端都注册过,否则在预渲染期间,是无法得到这个服务的。因此,如果需要在客户端和服务端注册多个同样的服务,建议将注册方法抽离出来,封装成静态方法。
-
静态方法
public static void ConfigureCommonServices(IServiceCollection services) { services.Add...; }
-
客户端-
Program.cs
var builder = WebAssemblyHostBuilder.CreateDefault(args); ... ConfigureCommonServices(builder.Services);
-
服务端-
Program.cs
var builder = WebApplication.CreateBuilder(args); ... Client.Program.ConfigureCommonServices(builder.Services);
三、注入服务
当服务成功注册后,就可以在组件或服务类中注入使用,关于服务类中使用注入的方式没啥好说的,直接通过构造函数注入使用就可以了,下面主要说一下组件的对于依赖服务的注入方式。
组件
组件需要通过@inject
来注入服务,如果是组件的隐藏类(内部C#类)则使用[Inject]
特性
-
组件
@page "/the-sunmakers" @inject IMyService MyService ......
-
组件的隐藏类
public partial class Sunmakers { [Inject] //[Inject(Key = "my-service")]可以指定具名服务 protected IMyService MyService { get; set; } = default!; }
组件基类与继承
此外,如果是组件内部类(IComponent
的子类),也可以使用[Inject]
特性对应服务类型的属性注入服务实例。
-
一般情况下不会使用这个特性,只在组件需要继承基类,并且基类也要注入服务时使用。
-
组件基类
using Microsoft.AspNetCore.Components; public class ComponentBase : IComponent { [Inject] //[Inject(Key = "my-service")]可以指定具名服务 protected IDataAccess DataRepository { get; set; } = default!; ... }
-
组件子类
@page "/demo" @inherits ComponentBase //此时使用@inherits就可以了 <h1>Demo Component</h1>
四、组件共存亡的Scoped服务
服务器端开发支持跨 HTTP 请求,但不支持跨客户端上加载的组件之间 SignalR 连接/线路消息的 Scoped
生命周期。 在页面或视图之间或从页面或视图导航到组件时,应用的 Razor 页面或 MVC 部分会正常处理作用域服务并在每个 HTTP 请求上重新创建服务。
- 对于服务端而言,如果是http请求,那么注入的作用域服务会正常的在请求发起时新建,请求结束时销毁。
- 对于服务端而言,如果是服务端交互式渲染的组件,那么是通过SingnalR来进行长链接的,注入的作用域服务与连接相关联,当我们在页面中进行导航切换组件时,撤离的组件在一定时间内其SingnalR还没断开,因此其注入的作用域服务的存活期要比想象中的长一些。
举个例子,在服务端项目中创建MyTime类型,并在Program.cs
中注册为作用域服务,然后在Pages
文件夹下新建ScopeServer组件。
-
MyTime.cs
public class ScopeTest { public DateTime CreateTime { get; set; } = DateTime.Now; }
-
Program.cs
...... builder.Services.AddScoped<MyTime>(); ......
-
ScopeServer.razor
@page "/scope-server" @rendermode InteractiveServer @inject MyTime _myTime <p> @_myTime.CreateTime </p>
然后,启动服务,访问ScopeServer组件,从ScopeServer组件导航到其他组件页面并且在较短的间隔内导航回来,会发现上面的时间并没有发生变化,也就是用的一直是同一个实例。
如果我们希望,作用域服务的存活期跟组件存活期同步,可以通过两种方法,继承OwningComponentBase
类型并使用其ScopedServices.GetRequiredService<TService>()
方法创建服务,或直接继承OwningComponentBase<TService>
类型,使用其Service
属性。
OwningComponentBase<TService>
是OwningComponentBase
的子类,因此也可以使用ScopedServices.GetRequiredService<T>()
方法。
继承OwningComponentBase
-
ScopeServer.razor
@page "/scope-server" @rendermode InteractiveServer @inherits OwningComponentBase <p> @_myTime.CreateTime </p> @code{ private MyTime? _myTime; protected override void OnInitialized() { _myTime = ScopedServices.GetRequiredService<MyTime>(); } }
继承OwningComponentBase<TService>
-
ScopeServer.razor
@page "/scope-server" @rendermode InteractiveServer @inherits OwningComponentBase<MyTime> <p> @Service.CreateTime </p>
客户端项目
对于客户端项目而言,是没有作用域服务的概念的,其注册的Scoped
服务全都被视为单一实例,如果需要将其与组件生存周期一样,那么处理手法跟服务端是一样的。
默认服务
一、HttpClient
HttpClient
服务提供用于发送 HTTP 请求以及从 URI 标识的资源接收 HTTP 响应的方法。
1、服务端使用
在服务端项目中,默认情况下是不会自动注册HttpClient
服务的,因此如果需要在服务端中使用,需要自己在Program.cs
中使用AddHttpClient()
进行注册。
- 注意,注册的
HttpClient
为作用域服务,而不是单一实例。
基本使用
在进行HttpClient
时,可以直接使用AddHttpClient()
注册IHttpClientFactory
,而无需进行配置。
-
注册-Program.cs
var builder = WebApplication.CreateBuilder(args); builder.Services.AddRazorComponents() .AddInteractiveServerComponents() .AddInteractiveWebAssemblyComponents(); //注册HttpClient服务 builder.Services.AddHttpClient(); var app = builder.Build(); ......
当需要在组件中使用HttpClient
时候,可以通过@inject
注入工厂对象IHttpClientFactory
来创建HttpClient
。
-
注入
@page "/http-test" @using Microsoft.Net.Http.Headers @rendermode InteractiveAuto @inject IHttpClientFactory _httpClientFactory <h3>HttpClientTest</h3> @code { protected override async Task OnInitializedAsync() { //进行http请求信息的封装 var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, "https://api.github.com/repos/dotnet/AspNetCore.Docs/branches") { Headers = { { HeaderNames.Accept, "application/vnd.github.v3+json" }, { HeaderNames.UserAgent, "HttpRequestsSample" } } }; var httpClient = _httpClientFactory.CreateClient(); //创建HttpClient var httpResponseMessage = await httpClient.SendAsync(httpRequestMessage); //发送请求 //...... } }
HttpClient
的命名使用(推荐)
如果项目中需要用到多个不同配置的HttpClient
,或者说希望创建得到的HttpClient
对象本身已经做了一些基础配置,可以使用命名注册的方式。
-
命名注册-Program
builder.Services.AddHttpClient("GitHub", httpClient => { httpClient.BaseAddress = new Uri("https://api.github.com/"); httpClient.DefaultRequestHeaders.Add(HeaderNames.Accept, "application/vnd.github.v3+json"); httpClient.DefaultRequestHeaders.Add(HeaderNames.UserAgent, "HttpRequestsSample"); });
然后在组件中通过@inject
注入工厂对象IHttpClientFactory
通过不同的命名来创建不同的HttpClient
对象。
-
注入
@page "/http-test" @using Microsoft.Net.Http.Headers @rendermode InteractiveAuto @inject IHttpClientFactory _httpClientFactory <h3>HttpClientTest</h3> @code { protected override async Task OnInitializedAsync() { var httpClient = _httpClientFactory.CreateClient("GitHub"); var result = await httpClient.GetAsync("repos/dotnet/AspNetCore.Docs/branches"); //..... } }
为什么要使用IHttpClientFactory
HttpClient
管理
IHttpClientFactory
调用CreateClient
都会返回一个新HttpClient
实例。 每个命名客户端都创建一个HttpMessageHandler
,由工厂管理HttpMessageHandler
实例的生存期。IHttpClientFactory
将工厂创建的HttpMessageHandler
实例汇集到池中,以减少资源消耗。 新建HttpClient
实例时,可能会重用池中的HttpMessageHandler
实例(如果生存期尚未到期的话)。IHttpClientFactory
跟踪和处置HttpClient
实例使用的资源,因此HttpClient
实例通常可视为无需处置的 .NET 对象。- 这里的处置是指
Dispose
、取消请求等操作。
- 这里的处置是指
日志管理
- 通过
IHttpClientFactory
创建的客户端记录所有请求的日志消息。 在日志记录配置中启用合适的信息级别可以查看默认日志消息。 仅在跟踪级别包含附加日志记录(例如请求标头的日志记录)。
2、客户端使用
注册
客户端项目中使用HttpClient
与服务端项目中使用有些不同,由于客户端项目中并没有内置HttpClient
服务,所以没有提供AddHttpClient()
方法。因此需要使用AddScoped
方法来实现HttpClient
服务的注册。
-
注册-Program.cs
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
注入
由于客户端项目中是直接注册了作用域的HttpClient
服务,所以在注入时直接注入HttpClient
对象就可以了。
-
注入
@page "/http-test-client" @rendermode InteractiveAuto @inject HttpClient _httpClient <h3>HpTest</h3> @code { protected override async Task OnInitializedAsync() { var data = await _httpClient.GetFromJsonAsync<Data>("api/GetData"); } class Data { public string? Name { get; set; } public int Age { get; set; } } }
二、IJSRuntime
IJSRuntime
服务用于在Blazor项目中直接调用JS的方法,在Blazor Web App Auto项目中,服务端或客户端在默认情况下都已经对IJSRuntime
服务进行过注册了,所以在使用时候,直接注入IJSRuntime
对象即可。
生命周期
客户端:单一实例
服务端:作用域服务
使用示例
-
组件
@page "/js-test-server" @rendermode InteractiveServer @inject IJSRuntime JS <h3>HpTest</h3> @code { protected override async Task OnAfterRenderAsync(bool firstRender) { if (firstRender) { await JS.InvokeVoidAsync("confirm", "欢迎进入网站"); } } }
-
类
public class JSHandler { private IJSRuntime _js; public JSHandler(IJSRuntime jSRuntime) { _js = jSRuntime; } public async Task ConfirmAsync() { var result = await _js.InvokeAsync<bool>("confirm", "欢迎进入页面"); } }
三、NavigationManager
NavigationManager
服务用于处理 URI 和导航,在Blazor Web App Auto项目中,服务端或客户端在默认情况下都已经对NavigationManager
服务进行过注册了,所以在使用时候,直接注入NavigationManager
对象即可。
生命周期
客户端:单一实例
服务端:作用域服务
使用示例
-
组件
@page "/navigate" @rendermode InteractiveAuto @inject NavigationManager Navigation <h1>Navigate Example</h1> <button class="btn btn-primary" @onclick="NavigateToCounterComponent"> Navigate to the Counter component </button> @code { private void NavigateToCounterComponent() { Navigation.NavigateTo("counter"); } }
-
类
public class NavHandler { private NavigationManager _navigationManager; public NavHandler(NavigationManager navigationManager) { _navigationManager = navigationManager; } public void NavTo(string uri) { _navigationManager.NavigateTo(uri); } }
四、IWebAssemblyHostEnvironment
1、组件中读取环境
在客户端组件中,如果想要获取应用的环境信息,可以通过注入IWebAssemblyHostEnvironment
并读取 Environment
属性来得到。
@page "/read-environment"
@using Microsoft.AspNetCore.Components.WebAssembly.Hosting
@inject IWebAssemblyHostEnvironment Env
<h1>Environment example</h1>
<p>Environment: @HostEnvironment.Environment</p>
需要注意的是,对于Blazor Web App Auto应用,IWebAssemblyHostEnvironment
仅在客户端项目(.Client
)中进行了服务的注册,如果客户端项目中的组件允许预渲染,那么当组件在服务器上预渲染时,会由于找不到IWebAssemblyHostEnvironment
而在注入该服务时出现异常。
针对这个问题,可以在服务器项目上创建IWebAssemblyHostEnvironment
的自定义服务,然后进行注册:
-
ServerHostEnvironment.cs
public class ServerHostEnvironment(IWebHostEnvironment env, NavigationManager nav) : IWebAssemblyHostEnvironment { public string Environment => env.EnvironmentName; public string BaseAddress => nav.BaseUri; }
-
Program.cs
...... builder.Services.TryAddScoped<IWebAssemblyHostEnvironment, ServerHostEnvironment>(); ......
2、启动时读取客户端环境
在启动过程中,WebAssemblyHostBuilder
会通过HostEnvironment
属性公开 IWebAssemblyHostEnvironment
,因此可以在客户端项目的Program中,通过builder.HostEnvironment
来获取环境信息。
if (builder.HostEnvironment.Environment == "Development")
{
......
};
类似HostEnvironmentEnvExtensions
为IHostEnvironment
(服务端项目中Program
所使用的主机环境类型)提供了一系列便捷的扩展方法一样,WebAssemblyHostEnvironmentExtensions
则是为IWebAssemblyHostEnvironment
(客户端项目中Program
所使用的主机环境类型)提供了一系列便捷的扩展方法,可在当前环境中检查 Development
、Production
、Staging
和自定义环境名称
bool IsDevelopment()
bool IsProduction()
bool IsStaging()
bool IsEnvironment(envName)
if (builder.HostEnvironment.IsStaging())
{
......
};
if (builder.HostEnvironment.IsEnvironment("Custom"))
{
......
};