razordependency-injectionblazorinversion-of-control

Blazor/Razor resolve components using dependency injection


I need an equivalent to dependeny injection to resolve Blazor/Razor components.

That is, I want to decouple my components in Razor Class Libraries in the exact same way that DI / IoC allows us to decouple back-end services. Does anyone know how I can achive this?

I an NOT asking how to use DI to resolve services within blazor/razor components.

Example

Imagine a large blazor project with three areas, each with their own Razor Class Library:

  1. students
  2. teachers
  3. rooms

I can decouple these three areas nicely into three separate projects. Dependency injection allows me to pull together the service dependencies nicely in the final project. For example, the razor page Page_Student_View can nicely consume IRoomService, the implementation of which comes from the rooms project using dependency injection.

I cannot, however, find any method to achieve equivalent decoupling with Razor components. What I would like is something like:

  @* Inside the student project *@
  @page "/student/view
  <div>
      My tearcher is <ITeacherInlineDisplay TeacherId="@Student.TeacherId"/>
      <br/>
      My room is <IRoomInlineDisplay RoomId="@Student.RoomId"/>
  </div>

The interfaces are defined in a common class library:

   /* Inside the Common project */
   public interface ITeacherInlineDisplay : IComponent 
   {
        [Parameter] int? TeacherId { get; set;} 
   }

Implement an implementation of the room display control inside the teacher project:

  @* Inside the Teacher project *@
  @implements ITeacherInlineDisplay 
  <div>
      @Teacher.Name
      @* Complex stuff in here: context menu, drag + drop etc. *@
  </div>
  @code {
      [Parameter] int? TeacherId { get; set;} 
  }

What I have tried / possible workarounds

  1. I've been searching for a long time. Results always explain using DI to inject services into components - which is not my question.
  2. I could make this work using DynamicComponent. I have two concerns with this. First is the inefficiency due to the additional (I presume fairly expensive) abstraction layer. The second is around compile time validation / checks / intellisense to catch parameter errors.
  3. I have tried using interfaces inheriting from IComponent. But that doesn't seem to work. It wasn't intedend for that purposes - more for implementing my own components using that interface, rather than calling by using that interface.

I could quite quickly implement 2 - and problably will if noone has a better solution. I considered patching the Blazor/Razor source to make it resove the IComponent when compiling razor pages. A big job which I don't think is worth me doing.

I just can't believe that I'm the only person who has this need - surley there is broader need for this kind of archiecture?

Background Information

I have a fairly large ASP.NET Blazor server-side application. I'm having trouble managing dependencies between razor/Blazor components.

This project has grown very rapidly without a coherent plan - more of a prototype project which has gone live with the customer and just grown and grown. Blazor / EF Core have handled this really well. We refactor very often, relying on Visual Studio refactoring and EF Core migrations to just update everything as we go.

We are effectively both vertically sliced (by business areas) and horizontally sliced (data later / middle layers / UI components).

I'm running (and always will) latest version of the stack: Currently .NET 8 / ASP.NET Core 8 / EF Core 8.

If we had DI for razor components, I would consider implementing testing at the UI level. Without that (and being able to Mock/Stub in controls) I'm not going to bother.


Solution

  • Disclaimer: this is only a proof of concept - not a suggestion that this would be good or bad - I leave that to you to decide.

    Component Activation

    Blazor has the ability to control component activation through an instance of the IComponentActivator interface.

    The default one is called DefaultComponentActivator if you care to inspect it.

    You can provide your own through DI like this:

    builder.Services.AddSingleton<IComponentActivator,MyComponentActivator>();
    

    MyComponentActivator is now used by the Renderer to create instances of Components as they are needed.

    A quick copy paste of the DefaultComponentActivator and a bit of tweaking to use DI gives you something like this:

    MyComponentActivator

    using System.Collections.Concurrent;
    using System.Diagnostics.CodeAnalysis;
    
    using Microsoft.AspNetCore.Components;
    
    public class MyComponentActivator(IServiceProvider serviceProvider) : IComponentActivator
    {
      public IComponent CreateInstance([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type componentType)
      {
        var resolvedType = serviceProvider.GetService(componentType);
        if (resolvedType is IComponent component)
        {
          return component;
        }
        return DefaultCreateInstance(componentType);
      }
    
      /* Code below here is (c) Microsoft - taken from DefaultComponentActivator */
    
      private static readonly ConcurrentDictionary<Type, ObjectFactory> _cachedComponentTypeInfo = new();
    
      public static void ClearCache() => _cachedComponentTypeInfo.Clear();
    
      /// <inheritdoc />
      public IComponent DefaultCreateInstance([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type componentType)
      {
        if (!typeof(IComponent).IsAssignableFrom(componentType))
        {
          throw new ArgumentException($"The type {componentType.FullName} does not implement {nameof(IComponent)}.", nameof(componentType));
        }
    
        var factory = GetObjectFactory(componentType);
    
        return (IComponent)factory(serviceProvider, []);
      }
    
      private static ObjectFactory GetObjectFactory([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type componentType)
      {
        // Unfortunately we can't use 'GetOrAdd' here because the DynamicallyAccessedMembers annotation doesn't flow through to the
        // callback, so it becomes an IL2111 warning. The following is equivalent and thread-safe because it's a ConcurrentDictionary
        // and it doesn't matter if we build a cache entry more than once.
        if (!_cachedComponentTypeInfo.TryGetValue(componentType, out var factory))
        {
          factory = ActivatorUtilities.CreateFactory(componentType, Type.EmptyTypes);
          _cachedComponentTypeInfo.TryAdd(componentType, factory);
        }
    
        return factory;
      }
    }
    

    The CreateInstance method has been written to use DI to locate and activate component instances - with a fallback to the default component activation when DI doesn't resolve anything.

    reader task: can we instead inject the default IComponentActivator into MyComponentActivator so we don't need to copy the code???

    Don't use Interfaces

    When you write Blazor code, the compiler requires design time knowledge of components, and interfaces won't work.

    You can't simply write

    <ITeacherInlineDisplay/>

    as it will not be recognised as a component.

    Extend a base component

    Create a bare bones base component TeacherInlineDisplay (in a shared library as you would for the interface) which has the "interface" you want to use ( e.g. virtual parameters ) TeacherInlineDisplay

    public class TeacherInlineDisplay : ComponentBase
    {
      [Parameter] public virtual int? TeacherID { get; set; }
    }
    

    And then inherit/extend that base class in your Teacher project/components.

    e.g. TeacherInlineLocal

    @* Inside the Teacher project *@
    @inherits TeacherInlineDisplay 
    <div>
        @Teacher.Name
        @* Complex stuff in here: context menu, drag + drop etc. *@
    </div>
    @code {
        [Parameter] public override int? TeacherId { get; set;} 
    }
    

    Wrapping it all up

    You can now register component implementations in DI like this:

    builder.Services.AddTransient<TeacherInlineDisplay, TeacherInlineLocal>();
    

    And place the base component on the page like this:

    @* Inside the student project *@
    @page "/student/view
    <div>
        My tearcher is <TeacherInlineDisplay TeacherId="@Student.TeacherId"/>
        <br/>
    </div>
    

    And the component TeacherInlineDisplay will be resolved from DI.