如何使用Razor组件实现纯DI

人气:680 发布:2022-10-16 标签: dependency-injection c# razor asp.net-core razor-pages

问题描述

我正在使用Dependency Injection Principles, Practices, and Patterns(DIPP&;P)一书中介绍的纯依赖注入方法制作一个ASP.NET核心应用程序。我的应用程序的一部分有一个Web API控制器。要用我的控制器实现Pure DI,我可以很容易地按照DIPP&;P中的7.3.1;创建自定义控制器激活器来创建控制器激活器类similar to the example found in DIPP&P。这是通过实现IControllerActivator并在create方法中合成我的合成根来完成的。

我的应用程序还将以Razor组件为特色。我希望继续使用纯依赖注入方法,但我找不到任何有关如何做到这一点的示例。

我的问题是:

是否可以使用Razor组件实现纯DI? 如果是这样的话,我们该如何做?

推荐答案

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

让我们从作为作曲者的自定义IComponentActivator开始,它是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);
        }
    }
}

注意此实现的一些事项:

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

与内置行为相比,使用Pure 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的任何属性都是从内置DI容器拉入的。但是,由于您不会在容器中注册您自己的类(因为您应用的是Pure DI),所以这不会起作用,并且您的页面将中断。

所需的最后一个缺少的基础设施是将WeatherComponentActivator注册到框架的DI容器中。这是在Startup类中完成的:

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类中是不可能完成的。

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

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现在可以注册为Singleton:

services.RegisterSingleton(new WeatherComposer());

837