为什么应该用record来定义DTO(续)

Posted dotNET跨平台

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了为什么应该用record来定义DTO(续)相关的知识,希望对你有一定的参考价值。

前言

上次,我们介绍了因为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重写EqualsGetHashCode也不是个事。

record的相等性

其实,更简单的解决方法是修改定义如下:

public record GetUserByIdQuery : IRequest<UserDto>
{
    public int Id { get; set; }
}

你会发现测试通过了。

这是因为,record在设计上就具备创建具有值相等数据类型的能力,编译器会自动生成样板代码

结论

在本文中,我们介绍了通过使用record类型,可以大大简化定义实现值相等性DTO的代码量。

如果你觉得这篇文章对你有所启发,请关注我的个人公众号”My IO“,记住我!

以上是关于为什么应该用record来定义DTO(续)的主要内容,如果未能解决你的问题,请参考以下文章

为什么应该用record来定义DTO

服务层应该接受来自控制器的 DTO 或自定义请求对象吗?

用简单的 POJO 或 DTO 对象替换这个持久化实体?

(扫盲)DTO数据传输对象

我可以重用代码来为 EF Core 的子属性选择自定义 DTO 对象吗?

DTO中的继承和自定义逻辑[关闭]