Blazor/Razor 使用依赖注入解析组件

问题描述 投票:0回答:1

我需要相当于依赖注入的来解决 Blazor/Razor 组件。

也就是说,我想以与 DI / IoC 允许我们解耦后端服务完全相同的方式解耦 Razor 类库中的组件。 有谁知道我怎样才能实现这个目标?

不是问如何使用 DI 来解析 blazor/razor 组件中的服务。

示例

想象一个包含三个区域的大型 blazor 项目,每个区域都有自己的 Razor 类库:

  1. 同学们
  2. 老师们
  3. 房间

我可以将这三个领域很好地分解为三个独立的项目。依赖注入使我能够在最终项目中很好地整合service依赖项。 例如,razor 页面

Page_Student_View
可以很好地使用
IRoomService
,其实现来自使用依赖注入的 rooms 项目。

但是,我无法找到任何方法来实现与 Razor 组件的等效解耦。 我想要的是这样的:

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

接口定义在公共类库中:

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

在教师项目内实现房间显示控制的实现:

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

我尝试过/可能的解决方法

  1. 我找了很久。结果总是解释使用 DI 将服务注入组件 - 这不是我的问题。
  2. 我可以使用 DynamicComponent 来完成这项工作。我对此有两个担忧。首先是由于额外的(我认为相当昂贵的)抽象层而导致效率低下。第二个是围绕编译时验证/检查/智能感知来捕获参数错误。
  3. 我尝试过使用继承自 IComponent 的接口。但这似乎不起作用。它并不是为了这个目的 - 更多的是使用该接口实现我自己的组件,而不是使用该接口进行调用。

我可以很快实现 2 - 如果没有人有更好的解决方案,我可能会这样做。 我考虑修补 Blazor/Razor 源,使其在编译 razor 页面时解析 IComponent。这是一项我认为不值得我做的大工作。

我简直不敢相信我是唯一有这种需求的人 - 萨利对这种架构有更广泛的需求吗?

背景信息

我有一个相当大的 ASP.NET Blazor 服务器端应用程序。我在管理 razor/Blazor 组件之间的依赖关系时遇到问题。

这个项目发展得非常迅速,但没有一个连贯的计划——更多的是一个原型项目,它已经与客户一起上线,并且不断发展壮大。 Blazor / EF Core 很好地处理了这个问题。我们经常重构,依靠 Visual Studio 重构和 EF Core 迁移来更新所有内容。

我们有效地进行了垂直切片(按业务领域)和水平切片(数据后期/中间层/UI 组件)。

我正在运行(并且始终会)最新版本的堆栈:当前为 .NET 8 / ASP.NET Core 8 / EF Core 8。

如果我们有 razor 组件的 DI,我会考虑在 UI 级别实施测试。如果没有这个(并且能够在控件中模拟/存根)我就不会打扰。

razor dependency-injection blazor inversion-of-control
1个回答
0
投票

免责声明:这只是一个概念证明 - 并不是好或坏的建议 - 我将其留给您来决定。

组件激活

Blazor 能够通过

IComponentActivator
接口的实例控制组件激活。

如果您愿意检查的话,默认的称为

DefaultComponentActivator

您可以通过 DI 提供您自己的:

builder.Services.AddSingleton<IComponentActivator,MyComponentActivator>();
渲染器现在使用

MyComponentActivator
来根据需要创建组件实例。

快速复制粘贴

DefaultComponentActivator
并进行一些调整以使用 DI 会得到如下所示的结果:

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

CreateInstance
方法已编写为使用 DI 来定位和激活组件实例 - 当 DI 无法解决任何问题时,会回退到默认组件激活。

读者任务:我们是否可以将默认的 IComponentActivator 注入到 MyComponentActivator 中,这样我们就不需要复制代码???

不要使用接口

当您编写 Blazor 代码时,编译器需要组件的设计时知识,并且接口将无法工作。

你不能简单地写

<ITeacherInlineDisplay/>

因为它不会被识别为组件。

扩展基础组件

创建一个简单的基本组件

TeacherInlineDisplay
(在共享库中,就像在接口中一样),它具有您想要使用的“接口”(例如虚拟参数)
TeacherInlineDisplay

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

然后在您的

Teacher
项目/组件中继承/扩展该基类。

例如

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

把一切都包起来

您现在可以像这样在 DI 中注册组件实现:

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

并将基本组件放置在页面上,如下所示:

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

并且组件

TeacherInlineDisplay
将从 DI 解析。

© www.soinside.com 2019 - 2024. All rights reserved.