.NET 8主构造函数参数在单元测试中注入失效问题
.NET 8主构造函数在单元测试中依赖为空的原因分析
问题场景
在传统ASP.NET服务层的xUnit+Substitute单元测试场景中,常规构造函数注入依赖的代码运行正常。但在.NET 8(C# 12)项目里,使用Rider提示的主构造函数参数写法实现AppService后,单元测试中注入的IAppRepository对象在Get方法中为空,但实际运行时却正常。
代码示例
传统构造函数实现(Service.cs)
public class AppService : IAppService { private readonly IAppRepository _appRepository; public AppService(IAppRepository appRepository) { this._appRepository = appRepository; } public App Get(int id) { // 业务逻辑代码 return _appRepository.Get(id); } }
单元测试代码(AppServiceTests.cs)
public class AppServiceTests { public void Test() { IAppRepository mockAppRepository = Substitute.For<IAppRepository>(); // Mock逻辑设置 AppService service = new AppService(mockAppRepository); // 断言逻辑 } }
主构造函数参数实现(AppService.cs)
public class AppService(IAppRepository appRepository) : IAppService { // 无需定义私有字段 public App Get(int id) { return appRepository.Get(id); } }
差异原因分析
主构造函数的合成字段特性
C# 12主构造函数允许直接在类中使用构造参数,编译器会自动生成一个私有合成字段(如<appRepository>__BackingField)来存储参数值。这个字段是隐式生成的,没有显式代码定义。单元测试项目的编译上下文差异
- 若单元测试项目的C#语言版本未设置为12,编译器会以旧语法逻辑处理主构造函数,导致参数无法正确绑定到合成字段,最终在方法调用时出现空引用。
- 部分情况下,Rider的代码优化或编译上下文同步问题,会导致单元测试项目生成的IL代码与主项目不一致,使得手动传入的依赖实例未被正确赋值到合成字段中。
DI容器与手动构造的执行逻辑差异
实际运行时通过DI容器注入时,容器会严格匹配主构造函数签名传递实例,且DI容器的初始化逻辑与主项目编译上下文一致,能正确识别并赋值合成字段;但单元测试中手动构造时,若编译上下文不一致,就会出现参数绑定失败的情况。
解决方案
- 确保单元测试项目的C#语言版本设置为12:在项目属性的“生成”选项中,将“语言版本”选择为“C# 12”。
- 显式定义私有字段存储主构造函数参数,避免隐式合成字段的兼容性问题:
public class AppService(IAppRepository appRepository) : IAppService { private readonly IAppRepository _appRepository = appRepository; public App Get(int id) { return _appRepository.Get(id); } }
内容的提问来源于stack exchange,提问作者Ming Sun
相关产品推荐
相关产品推荐

