Dependency Injected Blazor App

Dependency Injected Blazor App

Blazor Does Not Support Constructor Injection

Or does it?

Code - gimme code

The v5.0.0-preview.8.20414.8 release included a nice refactor commit from Mladen Macanović that introduced the IComponentActivator interface (along with a default implementation).

It's a simple interface

/// <summary>
/// Represents an activator that can be used to instantiate components.
/// The activator is not responsible for dependency injection, since the framework
/// performs dependency injection to the resulting instances separately.
/// </summary>
public interface IComponentActivator
{
    /// <summary>
    /// Creates a component of the specified type.
    /// </summary>
    /// <param name="componentType">The type of component to create.</param>
    /// <returns>A reference to the newly created component.</returns>
    IComponent CreateInstance(Type componentType);
}

The default implementation uses Activator.CreateInstance to create new components, which is the way the Blazor team designed it, and which is the reason you can't use constructor injection.

public IComponent CreateInstance(Type componentType)
{
    var instance = Activator.CreateInstance(componentType);
    if (!(instance is IComponent component))
    {
        throw new ArgumentException($"The type {componentType.FullName} does not implement {nameof(IComponent)}.", nameof(componentType));
    }

    return component;
}

What if we - as a simple test - add some DI to that? Note: this is just an experiment - there are doubtless going to be better ways

Create our own IComponentActivator and let DI inject an IServiceProvider so we can resolve Components.

public class MyActivator : IComponentActivator
{
    public MyActivator(IServiceProvider serviceProvider)
    {
        ServiceProvider = serviceProvider;
    }

    IServiceProvider ServiceProvider { get; }
}

Now, lets change the method to get Components from the ServiceProvider:

    public IComponent CreateInstance(Type componentType)
    {
        // Attempt to get a component from the ServiceProvider
        var spInstance = ServiceProvider.GetService(componentType);
        if (spInstance is IComponent spComponent)
        {
            return spComponent;
        }

        // Attempt to create it the original Blazor way
        var instance = Activator.CreateInstance(componentType);
        if (instance is IComponent component)
        {
            return component;
        }

        // That's not a known component!
        throw new ArgumentException($"The type {componentType.FullName} does not implement {nameof(IComponent)}.", nameof(componentType));
    }

Notice there is a fallback to the Activator.CreateInstance method of construction - in this case because I don't want to put all my Components into the DI container - it's just an experiment, not a torture device!

Also notice, in this naive implementation, there is no abstraction using interfaces - Blazor passes us an implementation type, so we can't ask DI for an interface implementation. That experiment would be for another time!

That also means we have to register the implemented component in DI, not using an interface.

    public void ConfigureServices(IServiceCollection services)
    {
        // other services...
        services.AddScoped<IComponentActivator, MyActivator>();
        services.AddTransient<Counter2>();
    }

We register our custom activator and our component in DI.

Now, our component can do this

    public partial class Counter2 
    {
        public Counter2(IJSRuntime jSRuntime)
        {
            JSRuntime = jSRuntime;
        }

        private IJSRuntime JSRuntime { get; }
        private int currentCount { get; set; }

        private async Task IncrementCount()
        {
            currentCount++;
            await JSRuntime.InvokeVoidAsync("alert", $"Counter: {currentCount}");
        }
    }

...and the required dependency IJSRuntime will be supplied by the DI system instead of using the Inject attribute/directive.

Summary

This was just an experiment to see where we are with constructor injection. It is possible in a limited way.