如何通过依赖注入实现Blazor/Razor组件的解耦与解析?
核心需求
要实现基于接口解析Blazor组件实现的机制,达成Razor类库(RCL)间的UI组件解耦,逻辑完全对应后端服务用DI/IoC的解耦方式——不是在组件内部注入服务,而是让组件本身可以通过接口被解析、替换,实现跨RCL的依赖隔离。
举例场景:拆分students、teachers、rooms三个独立RCL后,学生项目中直接通过组件接口编写UI,无需依赖具体实现:
@* 学生RCL内部代码 *@ @page "/student/view" <div> My teacher is <ITeacherInlineDisplay TeacherId="@Student.TeacherId"/> <br/> My room is <IRoomInlineDisplay RoomId="@Student.RoomId"/> </div>
组件接口统一定义在公共类库:
/* 公共类库内部代码 */ public interface ITeacherInlineDisplay : IComponent { [Parameter] int? TeacherId { get; set;} }
具体组件实现放在对应RCL中(如teachers项目实现ITeacherInlineDisplay)。
已尝试方案及痛点
- 搜索结果均为组件内注入服务的方案,不匹配需求;
- 考虑过
DynamicComponent,但存在性能损耗顾虑,且缺失编译时参数校验; - 尝试继承
IComponent自定义接口,无法实现预期的组件解析效果。
使用环境:.NET 8/ASP.NET Core 8/EF Core 8 Blazor Server项目,实现该机制后可开展UI层测试。
可行解决方案:自定义组件解析器+DI注册
1. 注册组件接口与实现的DI映射
在项目启动类(Program.cs)中,将组件接口和对应实现类注册到DI容器,逻辑与后端服务注册一致:
// 注册教师组件接口与实现 builder.Services.AddTransient<ITeacherInlineDisplay, TeacherInlineDisplay>(); // 注册教室组件接口与实现 builder.Services.AddTransient<IRoomInlineDisplay, RoomInlineDisplay>();
注:
TeacherInlineDisplay是teachersRCL中实现ITeacherInlineDisplay的具体组件类,需确保项目引用对应RCL。
2. 编写通用组件解析器
创建通用组件ComponentResolver.razor放在公共类库,负责从DI容器获取组件实例并渲染,同时传递参数:
@typeparam TComponent where TComponent : IComponent @inject IServiceProvider ServiceProvider @{ var component = ServiceProvider.GetRequiredService<TComponent>(); // 将父组件传入的参数转为ParameterView var parameters = ParameterView.FromDictionary(AdditionalAttributes); var renderHandle = new RenderHandle(Render); component.Attach(renderHandle); await component.SetParametersAsync(parameters); } @code { private void Render(RenderFragment fragment) { fragment.Invoke(__builder); } }
3. 业务组件中使用解析器(带编译时校验优化)
为了避免AdditionalAttributes的弱类型问题,给每个组件接口编写强类型封装组件,以ITeacherInlineDisplay为例:
@using Your.Common.Library.Interfaces @inherits ComponentResolver<ITeacherInlineDisplay> @code { [Parameter] public int? TeacherId { get; set; } protected override void OnParametersSet() { AdditionalAttributes = new Dictionary<string, object?> { { nameof(TeacherId), TeacherId } }; base.OnParametersSet(); } }
将该组件命名为TeacherInlineDisplayResolver.razor放在公共类库,学生项目中即可用强类型方式调用,同时获得编译时参数校验:
@* 学生RCL内部代码 *@ @page "/student/view" @using Your.Common.Library.Resolvers <div> My teacher is <TeacherInlineDisplayResolver TeacherId="@Student.TeacherId"/> <br/> My room is <RoomInlineDisplayResolver RoomId="@Student.RoomId"/> </div>
性能说明
该方案比DynamicComponent更高效:
- 直接从DI获取实例,避免反射开销;
- 强类型封装组件提供编译时校验,杜绝参数名错误导致的运行时问题。
内容的提问来源于stack exchange,提问作者user3386507

