第二章 基础知识(3) - 依赖注入

依赖注入是一种设计模式,在类及其依赖关系之间实现控制反转(IOC)的技术。如果想要彻底理解依赖注入的设计模式,则先要清楚如下几个概念:

  • 依赖倒置原则,简称DIP
  • 控制反转,简称IOC
  • 依赖注入,简称DI
  • 控制反转容器,简称IOC容器

Blazor web App项目中已经将依赖注入的设计模式加入其中了,因此在整个框架中,依赖注入无处不在。对于框架本身,内置了许多的组件服务供开发者使用,例如配置组件、日志组件、选项组件等,也可以将自己定义的服务进行注册。

Blazor 项目中,服务的注册默认是在Program中进行的。

服务的注册与注入

一、服务的生存期限

在Blazor Web App项目中,服务的注册是通过IServiceCollection接口来进行的,该接口主要提供了三种类型的注册方法,即AddScopedAddSingleton以及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")
{
    ......
};

类似HostEnvironmentEnvExtensionsIHostEnvironment(服务端项目中Program所使用的主机环境类型)提供了一系列便捷的扩展方法一样,WebAssemblyHostEnvironmentExtensions则是为IWebAssemblyHostEnvironment(客户端项目中Program所使用的主机环境类型)提供了一系列便捷的扩展方法,可在当前环境中检查 DevelopmentProductionStaging 和自定义环境名称

  • bool IsDevelopment()
  • bool IsProduction()
  • bool IsStaging()
  • bool IsEnvironment(envName)
if (builder.HostEnvironment.IsStaging())
{
    ......
};

if (builder.HostEnvironment.IsEnvironment("Custom"))
{
    ......
};
  • 13
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

SchuylerEX

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值