如何使用 Razor 组件实现纯 DI

How to implement Pure DI with Razor Components

我正在使用 Dependency Injection Principles, Practices, and Patterns (DIPP&P). Part of my application has a web API controller. To implement Pure DI with my controller, I was easily able to follow section 7.3.1 "Creating a custom controller activator" from DIPP&P to create a controller activator class, similar to the example found in DIPP&P 书中解释的纯 DI 方法制作一个 ASP.NET 核心应用程序。这是通过实施 IControllerActivator 并在 create 方法中组合我的组合根来完成的。

我的应用程序还将具有 Razor 组件。我想继续使用纯 DI 方法,但我找不到任何有关如何执行此操作的示例。

我的问题是:

  1. 是否可以使用 Razor 组件实现纯 DI?
  2. 如果是这样,该如何处理?

当然可以将 Pure DI 应用于 Razor 应用程序,但不是通过 IRazorPageActivator 而是通过 IComponentActivator 抽象。下面是一个基于默认 Visual Studio (2019) Razor 项目模板的示例。由于该模板是围绕天气预报域构建的,因此我们以它为例。

让我们从自定义 IComponentActivator 开始,它作为您的 Composer,是您 Composition Root 的一部分。

public record WeatherComponentActivator(IServiceProvider Provider)
    : IComponentActivator
{
    public IComponent CreateInstance(Type componentType) => (IComponent)this.Create(componentType);

    private object Create(Type type)
    {
        switch (type.Name)
        {
            case nameof(FetchData):
                return new FetchData(new WeatherForecastService());

            case nameof(App): return new App();
            case nameof(Counter): return new Counter();
            case nameof(MainLayout): return new MainLayout();
            case nameof(NavMenu): return new NavMenu();
            case nameof(Pages.Index): return new Pages.Index();
            case nameof(SurveyPrompt): return new SurveyPrompt();

            default:
                return type.Namespace.StartsWith("Microsoft")
                    ? Activator.CreateInstance(type) // Default framework behavior
                    : throw new NotImplementedException(type.FullName);
        }
    }
}

注意此实现的一些事项:

  • 构造函数注入用于 FetchData Razor 页面。
  • 它回退到对框架组件使用 Activator.CreateInstanceActivator.CreateInstance 是框架用于其默认组件激活器实现的行为。回退是必需的,因为要创建相当多的框架 Razor 组件。你不想一一列举,也不是你能管理的。
  • WeatherComponentActivatorclass中,我注入了一个IServiceProvider。现在不需要它,但它表明如果需要,可以使用它来引入框架依赖项。肯定会有一段时间您的页面直接或间接需要依赖框架的某些部分。这就是如何将两者结合起来。该框架与其 DI 基础结构紧密相连, 将 DI 容器用于其框架部分是不可能的。

与 built-in 行为相比,使用纯 DI 具有能够在 Razor 页面上使用构造函数注入的优点。对于您的 FetchData 组件,这意味着它应该按如下方式实现:

@page "/fetchdata"

@using AspNetCoreBlazor50PureDI.Data
@* "notice that there are no @inject attributes here" *@
<h1>Weather forecast</h1>
<p>This component demonstrates fetching data from a service.</p>
@if (forecasts == null)
{
    <p><em>Loading...</em></p>
}
else
{
    <table class="table">
        Default template code removed for brevity
    </table>
}

@code {
    private readonly WeatherForecastService forecastService;

    // Yeah! Constructor Injection!
    public FetchData(WeatherForecastService forecastService)
    {
        this.forecastService = forecastService;
    }

    private WeatherForecast[] forecasts;

    protected override async Task OnInitializedAsync()
    {
        forecasts = await this.forecastService.GetForecastAsync(DateTime.Now);
    }
}

请注意,即使您的 WeatherComponentActivator 现在可以控制 Razer 组件的创建,Razor 框架仍会尝试初始化它们。这意味着页面上标有 @inject 的任何属性都是从 built-in DI 容器中提取的。但是由于您不会在容器中注册自己的 classes(因为您正在应用 Pure DI),这将不起作用并且您的页面会中断。

最后缺少的基础设施是将 WeatherComponentActivator 注册到框架的 DI 容器中。这是在 Startup class:

内部完成的
public class Startup
{
    public Startup(IConfiguration configuration) => this.Configuration = configuration;

    public IConfiguration Configuration { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddRazorPages();
        services.AddServerSideBlazor();

        // Register your custom component activator here
        services.AddScoped<IComponentActivator, WeatherComponentActivator>();
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env) ...
}

注意 WeatherComponentActivator 是如何注册为 Scoped 组件的。如果您需要从 IServiceProvider 中引入框架组件,这一点很重要,因为它允许解析 Scoped 框架组件,而这在 Singleton class 中是不可能做到的。

但这有一些重要的后果。控制器激活器和其他 classes 的示例用作组合根,通常包含用于在其中存储单例的私有字段。当您将 WeatherComponentActivator 注册为 Scoped 时,这对 WeatherComponentActivator 不起作用,因为那些私有字段将在下一个范围内消失。在那种情况下,您应该将单例存储在 private static 字段中,但这会导致在单元测试中创建 WeatherComponentActivator 变得更加困难,而您更希望在 运行 中进行单元测试隔离。因此,如果这是一个问题,您可以将 WeatherComponentActivator 中的依赖项组合提取到它自己的 class 中,例如:

public record WeatherComponentActivator(WeatherComposer Composer, IServiceProvider Provider)
    : IComponentActivator
{
    // Activator delegates to WeatherComposer
    public IComponent CreateInstance(Type componentType) =>
        (IComponent)this.Composer.Create(componentType, this.Provider);

public class WeatherComposer
{
    // Singleton
    private readonly ILogger logger = new ConsoleLogger();

    public object Create(Type type, IServiceProvider provider)
    {
        switch (type.Name)
        {
            case nameof(FetchData):
                return new FetchData(new WeatherForecastService());
            ...
        }
    }
}

这个新的 WeatherComposer 现在可以注册为单例:

services.RegisterSingleton(new WeatherComposer());