前言
上次,我们介绍了因为DTO的“不变性”,应该用record来定义DTO。
今天,我们来说明用record来定义DTO的另一个好处。
问题
首先,我们实现一个Controler,代码如下:
[ApiController]
[Route("[controller]")]
public class UserController : ControllerBase
{private readonly IMediator _mediator;public UserController(IMediator mediator){this._mediator = mediator;}[HttpGet("{id}")]public async Task<UserDto> GetById(int id){var request = new GetUserByIdQuery { Id = id };var result = await this._mediator.Send(request);return result;}
}public class UserDto
{public int Id { get; set; }public string Name{ get; set; }
}public class GetUserByIdQuery : IRequest<UserDto>
{public int Id { get; set; }
}
这里,IRequest<T>
可以认为是DTO。
然后,我们编写测试用例:
[Fact]
public async void Test1()
{var mediatorMock = new Mock<IMediator>();var request = new GetUserByIdQuery { Id = 1};var expectedUser = new UserDto { Id = 1, Name = "My IO" };mediatorMock.Setup(x => x.Send(request, default(CancellationToken))).Returns(Task.FromResult(expectedUser));var controller = new UserController(mediatorMock.Object);var result = await controller.GetById(1);Assert.Equal(expectedUser, result);
}
我们Mock了IMediator
,期望它执行Send
后返回expectedUser。
看起来都没有问题,但是测试执行失败:
调试代码,可以看到传递的参数是正确的,但是返回值是null:
这说明实际没有命中mediatorMock.Setup中的方法。
这是为什么呢?
原因
原因其实是,x.Send(request, default(CancellationToken))
表示必须完全匹配才能返回指定的结果,但是request
和GetById方法中创建的request
其实是2个不同的实例,.NET并不认为它们相等。
虽然可以修改mediatorMock.Setup方法来修复测试。
但对于我来说,属性值完全相同的DTO应该就是相等的,可以让类实现值相等性来解决:
public class GetUserByIdQuery : IRequest<UserDto>
{public int Id { get; set; }public override bool Equals(object obj) => this.Equals(obj as GetUserByIdQuery);public bool Equals(GetUserByIdQuery p){if (p is null){return false;}if (Object.ReferenceEquals(this, p)){return true;}if (this.GetType() != p.GetType()){return false;}return Id == p.Id;}public override int GetHashCode() => Id.GetHashCode();
}
但是,为每个DTO重写Equals
和GetHashCode
也不是个事。
record的相等性
其实,更简单的解决方法是修改定义如下:
public record GetUserByIdQuery : IRequest<UserDto>
{public int Id { get; set; }
}
你会发现测试通过了。
这是因为,record在设计上就具备创建具有值相等数据类型的能力,编译器会自动生成样板代码:
结论
在本文中,我们介绍了通过使用record类型,可以大大简化定义实现值相等性DTO的代码量。
如果你觉得这篇文章对你有所启发,请关注我的个人公众号”My IO“,记住我!