.NET 7 C#控制器测试含MediatR循环时返回500错误求助
测试ContactController的ReturnsSuccessResult()用例时出现500内部服务器错误,移除GetContactListQueryHandler中映射州ID与缩写的foreach循环后测试恢复正常,需定位问题原因。
相关代码
ContactListVm
public class ContactListVm { public Guid ContactId { get; set; } public string FirstName { get; set; } = string.Empty; public string LastName { get; set; } = string.Empty; public string NameSuffix {get; set;} = string.Empty; public string? MiddleName { get; set; } public string? CompanyName { get; set; } public string? Title { get; set; } public string Address1 { get; set; } = string.Empty; public string? Address2 { get; set; } public string? Address3 { get; set; } public string City { get; set; } = string.Empty; public Guid StateId { get; set; } public string? StateAbbreviation { get; set; } public string ZipCode { get; set; } = string.Empty; public string? Email { get; set; } public string PhoneNumber { get; set; } = string.Empty; public string? Fax { get; set; } }
GetContactListQuery
public class GetContactListQuery : IRequest<List<ContactListVm>> { }
GetContactListQueryHandler
public GetContactListQueryHandler(IMapper mapper, IContactRepository contactRepository, IStateRepository stateRepository) { _mapper = mapper; _contactRepository = contactRepository; _stateRepository = stateRepository; } public async Task<List<ContactListVm>> Handle(GetContactListQuery request, CancellationToken cancellationToken) { var allContacts = (await _contactRepository.ListAllAsync()) .OrderBy(c => c.LastName); var newContactList = new List<ContactListVm>(); foreach (var contact in allContacts) // 移除该循环后错误消失 { var contactDetailDto = _mapper.Map<ContactListVm>(contact); if (contactDetailDto.StateId != Guid.Empty) { var state = await _stateRepository.GetByIdAsync(contactDetailDto.StateId); contactDetailDto = _mapper.Map(state, contactDetailDto); } newContactList.Add(contactDetailDto); } return newContactList; }
Contact Controller
[Route("api/[controller]")] [ApiController] public class ContactController : ControllerBase { private readonly IMediator _mediator; public ContactController(IMediator mediator) { _mediator = mediator; } [HttpGet("all", Name ="GetAllContacts")] [ProducesResponseType(StatusCodes.Status200OK)] public async Task<ActionResult<List<ContactListVm>>> GetAllContacts() { var dtos = await _mediator.Send(new GetContactListQuery()); return Ok(dtos); } [HttpGet("{id}", Name = "GetContactById")] public async Task<ActionResult<ContactDetailVm>> GetContactById(Guid id) { var getContactDetailQuery = new GetContactDetailQuery() { Id = id }; return Ok(await _mediator.Send(getContactDetailQuery)); } [HttpPost(Name = "AddContact")] public async Task<ActionResult<CreateContactCommandResponse>> Create ([FromBody] CreateContactCommandResponse createContactCommand) { var response = await _mediator.Send(createContactCommand); return Ok(response); } }
Contact Controller Tests
[Collection("Controllers Test Collection")] public class ContactControllerTests : IClassFixture<CustomWebApplicationFactory<Program>> { private readonly CustomWebApplicationFactory<Program> _factory; private readonly Mock<IMediator> _mediatorMock; private readonly ContactController _contactController; public ContactControllerTests(CustomWebApplicationFactory<Program> factory) { _factory = factory; _mediatorMock = new Mock<IMediator>(); _contactController = new ContactController(_mediatorMock.Object); } [Fact] public async Task ReturnsSuccessResult() //<-- 此测试出现500内部服务器错误 { var client = _factory.GetAnonymousClient(); var response = await client.GetAsync("/api/contact/all"); response.EnsureSuccessStatusCode(); var responseString = await response.Content.ReadAsStringAsync(); var result = JsonSerializer.Deserialize<List<ContactListVm>>(responseString); Assert.IsType<List<ContactListVm>>(result); Assert.NotEmpty(result); }
GetContactListQueryHandlerTests
public class GetContactListQueryHandlerTests { private readonly IMapper _mapper; private readonly Mock<IContactRepository> _mockContactRepository; private readonly Mock<IStateRepository> _mockStateRepository; public GetContactListQueryHandlerTests() { var contactRepositoryMock = new ContactRepositoryMock(); _mockContactRepository = contactRepositoryMock.GetContactRepositoryMock(); var stateRepositoryMock = new StateRepositoryMock(); _mockStateRepository = stateRepositoryMock.GetStateRepositoryMock(); var configurationProvider = new MapperConfiguration(cfg => { cfg.AddProfile<ContactMappingProfile>(); cfg.AddProfile<StateMappingProfile>(); }); _mapper = configurationProvider.CreateMapper(); } [Fact] public async Task GetContactList_Result_IsOfTypeListContactListVm() { var handler = new GetContactListQueryHandler(_mapper, _mockContactRepository.Object, _mockStateRepository.Object); var result = await handler.Handle(new GetContactListQuery(), CancellationToken.None); result.ShouldBeOfType<List<ContactListVm>>(); } }
核心问题点定位
移除foreach循环后测试恢复正常,说明错误出在循环内的逻辑,重点排查以下几点:
StateRepository依赖未正确Mock
集成测试用CustomWebApplicationFactory启动了完整应用,而非控制器测试中创建的_mediatorMock。如果测试环境中IStateRepository无法找到对应StateId的数据,会导致GetByIdAsync返回null,后续AutoMapper映射可能抛出空引用异常。AutoMapper映射错误
当state为null时,调用_mapper.Map(state, contactDetailDto)可能触发异常,需检查StateMappingProfile是否处理了源为null的情况。异步操作的线程/上下文问题
循环内多次调用await _stateRepository.GetByIdAsync,如果测试环境中数据库连接或仓储实现存在异步处理问题,也可能导致500错误。
解决方案步骤
1. 查看测试环境的错误详情
修改测试代码,捕获并输出500错误的响应内容,明确异常信息:
[Fact] public async Task ReturnsSuccessResult() { var client = _factory.GetAnonymousClient(); var response = await client.GetAsync("/api/contact/all"); if (!response.IsSuccessStatusCode) { var errorContent = await response.Content.ReadAsStringAsync(); Assert.Fail($"请求失败,状态码:{(int)response.StatusCode},错误信息:{errorContent}"); } response.EnsureSuccessStatusCode(); // 后续断言逻辑... }
2. 确保测试环境中State数据存在
如果是真实数据库的集成测试,要保证测试数据中所有Contact的StateId都对应存在的State记录;如果使用Mock,需在CustomWebApplicationFactory中替换IStateRepository为Mock实现,确保GetByIdAsync返回有效数据。
3. 优化Handler逻辑,避免空引用
在Handler中增加null判断,防止state为null时触发映射异常:
if (contactDetailDto.StateId != Guid.Empty) { var state = await _stateRepository.GetByIdAsync(contactDetailDto.StateId); if (state != null) // 增加null判断 { contactDetailDto = _mapper.Map(state, contactDetailDto); } }
4. 批量获取State数据,提升性能与稳定性
循环内多次调用仓储查询低效且易出问题,建议一次性获取所有需要的State,再批量映射:
public async Task<List<ContactListVm>> Handle(GetContactListQuery request, CancellationToken cancellationToken) { var allContacts = (await _contactRepository.ListAllAsync()) .OrderBy(c => c.LastName); // 收集所有非空的StateId var stateIds = allContacts.Where(c => c.StateId != Guid.Empty) .Select(c => c.StateId) .Distinct() .ToList(); // 批量获取State数据 var states = await _stateRepository.GetByIdsAsync(stateIds); var stateDict = states.ToDictionary(s => s.StateId); var newContactList = allContacts.Select(contact => { var vm = _mapper.Map<ContactListVm>(contact); if (vm.StateId != Guid.Empty && stateDict.TryGetValue(vm.StateId, out var state)) { _mapper.Map(state, vm); } return vm; }).ToList(); return newContactList; }
内容的提问来源于stack exchange,提问作者TooStupidForStackOverflow

