Home > front end >  How to implement Pure DI with Razor Components
How to implement Pure DI with Razor Components

Time:02-15

I am making an ASP.NET Core application using the Pure DI approach explained in the book 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. This was done by implementing IControllerActivator and composing my composition root within the create method.

My application will also feature Razor Components. I would like to continue using the Pure DI approach but I cannot find any examples on how to do this.

My questions are:

  1. Is it possible to implement Pure DI with Razor Components?
  2. If so, how does one go about this?

CodePudding user response:

It certainly is possible to apply Pure DI to a Razor application, but not through IRazorPageActivator but through the IComponentActivator abstraction. Here's an example based on the default Visual Studio (2019) Razor project template. Since that template builds around a weather forecast domain, let's use that for the example.

Let's start with the custom IComponentActivator, which acts as your Composer, which is part of your 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);
        }
    }
}

Notice a few things with this implementation:

  • Constructor injection is used with the FetchData Razor page.
  • It falls back to using Activator.CreateInstance for framework components. Activator.CreateInstance is the behavior the framework uses for its default component activator implementation. Falling back is required, because there are quite some framework Razor components to be created. You don't want to list them all, and they are not yours to manage.
  • In the WeatherComponentActivator class, I injected an IServiceProvider. It's not needed right now, but it shows that it's possible to use it to pull in framework dependencies if you need. There will certainly be a time where your pages directly or indirectly need a dependency on some part of the framework. This is how to integrate the two. The framework is so tightly mangled with its DI infrastructure that it's impossible to not use the DI Container for its framework parts.

Compared to the built-in behavior, using Pure DI has the nice advantage of being able to use constructor injection on your Razor Pages. For your FetchData component, this means it should be implemented as follows:

@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 >
        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);
    }
}

Please be aware that even though your WeatherComponentActivator is now in control over the creation of Razer Components, the Razor framework will still try to initialize them. That means that any properties on the page that are marked with @inject are pulled in from the built-in DI container. But since you will not register your own classes in the container (since you are applying Pure DI), this won't work and your page will break.

The last missing piece of infrastructure required is the registration of your WeatherComponentActivator into the framework's DI Container. This is done inside the 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) ...
}

Notice how the WeatherComponentActivator is registered as a Scoped component. This important in case you need to pull in framework components from the IServiceProvider, because it allows resolving Scoped framework components, which is impossible to do from within a Singleton class.

But this has some important consequences. The examples of controller activators and other classes that function as a Composition Root often contain private fields that are used to store Singletons in them. This won't work for the WeatherComponentActivator when you register it as Scoped, because those private fields will be gone with the next scope. In that case you should either store singletons in private static fields, but that has the consequence that it becomes harder to create WeatherComponentActivator inside unit tests, where you rather want a unit test to run in isolation. So if that's an issue, you can extract the composition of dependencies out of the WeatherComponentActivator into its own class, for instance:

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());
            ...
        }
    }
}

This new WeatherComposer can now be registered as Singleton:

services.RegisterSingleton(new WeatherComposer());
  • Related