当类型是 C# 9 记录时,FluentAssertions Should().BeEquivalentTo() 在微不足道的情况下失败,似乎将对象视为字符串

Posted

技术标签:

【中文标题】当类型是 C# 9 记录时,FluentAssertions Should().BeEquivalentTo() 在微不足道的情况下失败,似乎将对象视为字符串【英文标题】:FluentAssertions Should().BeEquivalentTo() fails in trivial case when types are C# 9 records, seemingly treating objects as strings 【发布时间】:2021-05-06 11:58:00 【问题描述】:

我最近开始使用 FluentAssertions,它应该有这个强大的对象图比较功能。

我正在尝试做最简单的事情:将Address 对象的属性与AddressDto 对象的属性进行比较。它们都包含 4 个简单的字符串属性:Country、City、Street 和 ZipCode(它不是生产系统)。

谁能给我解释一下,比如我两岁,怎么了?

partnerDto.Address.Should().BeEquivalentTo(partner.Address)

它失败并显示以下消息:

消息:

预期结果。地址为 4 Some street, 12345 Toronto, Canada, 但找到 AddressDto Country = Canada, ZipCode = 12345, City = Toronto, Street = 4 Some street 。

有配置:

使用声明的类型和成员 按值比较枚举 按名称匹配成员(或抛出) 没有自动转换。 严格控制字节数组中的项目顺序

它似乎试图将Address 对象视为一个字符串(因为它覆盖了ToString()?)。我尝试使用options.ComparingByMembers<AddressDto>() 选项,但似乎没有任何区别。

AddressDtorecord,顺便说一句,不是 class,因为我正在用这个项目测试新的 .Net 5 功能;但它可能没什么区别。)


故事的寓意:

使用record而不是class会触发FluentAssertions,因为记录会在后台自动覆盖Equals(),并且FluentAssertions假定它应该使用Equals()而不是属性比较,因为覆盖的Equals()可能是为了提供所需的比较。

但是,在这种情况下,Equals()record 中的默认覆盖实现实际上仅在两种类型相同时才有效,因此它会失败,因此 FluentAssertions 在BeEquivalentTo() 上报告失败。

并且,在失败消息中,FluentAssertions 通过 ToString() 将对象转换为字符串,从而令人困惑地报告了该问题。这是因为记录具有“值语义”,因此它会这样对待它们。有一个open issue about this on GitHub。

我确认如果我将record 更改为class,则不会出现问题。

(我个人认为 FluentAssertions 在 record 上时应该忽略 Equals() 覆盖,并且两种类型不同,因为这种行为可以说不是人们所期望的。当时的当前问题发布时间,适用于 FluentAssertions 版本 5.10.3。)

我编辑了我的问题标题以更好地代表问题的实际情况,因此它可能对人们更有用。


参考资料:

正如人们所问的,这是域实体的定义(为了简洁起见,必须删除一些方法,因为我正在做 DDD,但它们肯定与问题无关):

public class Partner : MyEntity

    [Required]
    [StringLength(PartnerInvariants.NameMaxLength)]
    public string Name  get; private set; 

    [Required]
    public Address Address  get; private set; 

    public virtual IReadOnlyCollection<Transaction> Transactions => _transactions.AsReadOnly();
    private List<Transaction> _transactions = new List<Transaction>();

    private Partner()
     

    public Partner(string name, Address address)
    
        UpdateName(name);
        UpdateAddress(address);
    

    ...

    public void UpdateName(string value)
    
        ...
    

    public void UpdateAddress(Address address)
    
        ...
    

    ...


public record Address

    [Required, MinLength(1), MaxLength(100)]
    public string Street  get; init; 

    [Required, MinLength(1), MaxLength(100)]
    public string City  get; init; 

    // As I mentioned, it's not a production system :)
    [Required, MinLength(1), MaxLength(100)]
    public string Country  get; init; 

    [Required, MinLength(1), MaxLength(100)]
    public string ZipCode  get; init; 

    private Address()  

    public Address(string street, string city, string country, string zipcode)
        => (Street, City, Country, ZipCode) = (street, city, country, zipcode);

    public override string ToString()
        => $"Street, ZipCode City, Country";

这里是 Dto 等价物:

public record PartnerDetailsDto : IMapFrom<Partner>

    public int Id  get; init; 
    public string Name  get; init; 
    public DateTime CreatedAt  get; init; 
    public DateTime? LastModifiedAt  get; init; 

    public AddressDto Address  get; init; 

    public void Mapping(Profile profile)
    
        profile.CreateMap<Partner, PartnerDetailsDto>();
        profile.CreateMap<Address, AddressDto>();
    

    public record AddressDto
    
        public string Country  get; init; 
        public string ZipCode  get; init; 
        public string City  get; init; 
        public string Street  get; init; 
    

【问题讨论】:

您能否在edit 的问题中包含AddressAddressDto 的定义? 当然,@canton7;一会儿 partnerDto 变量的类型是什么? Address 属性是如何定义的? @Leaky 我们需要所有这些定义才能重现您的问题 仅供参考,关于这个github.com/fluentassertions/fluentassertions/issues/1451有一个未解决的问题 【参考方案1】:

您是否尝试过使用options.ComparingByMembers&lt;Address&gt;()

尝试将您的测试更改为:partnerDto.Address.Should().BeEquivalentTo(partner.Address, o =&gt; o.ComparingByMembers&lt;Address&gt;());

【讨论】:

其实这解决了这个问题。 @canton7 贴了一个很好的解释(可惜后来删了),建议用o.ComparingByMembers&lt;AddressDto&gt;(),没用。但是使用Address 作为参数确实有效。老实说,我不明白为什么;我还认为我应该用 dto 名称参数化这个方法。【参考方案2】:

我认为the docs的重要部分是:

要确定 Fluent Assertions 是否应该重复出现在对象的属性或字段中,它需要了解哪些类型具有值语义以及哪些类型应该被视为引用类型。默认行为是将覆盖 Object.Equals 的每种类型视为旨在具有值语义的对象

您的两条记录都覆盖了Equals,但它们的 Equals 方法只有在另一个对象属于同一类型时才会返回 true。所以我认为Should().BeEquivalentTo 看到你的对象实现了它们自己的相等性,调用(大概)AddressDto.Equals 返回 false,然后报告失败。

它使用两个记录的ToString() 版本报告失败,这两个记录返回 Country = Canada, ZipCode = 12345, City = Toronto, Street = 4 Some street (对于没有覆盖ToString 的记录)和4 Some street, 12345 Toronto, Canada,(对于带有覆盖ToString 的对象)。

正如文档所说,您应该可以使用 ComparingByMembers 覆盖它:

partnerDto.Address.Should().BeEquivalentTo(partner.Address,
   options => options.ComparingByMembers<Address>());

或全局:

AssertionOptions.AssertEquivalencyUsing(options => options
    .ComparingByMembers<Address>());

【讨论】:

这对我来说听起来很合理,但使用options =&gt; options.ComparingByMembers&lt;AddressDto&gt;() 并不会改变结果。 :// 编辑为使用 Address,根据 Matt Hope 的回答。将他们的答案视为第一个、正确、已接受的答案,并将我的答案视为添加额外的上下文 谢谢。我也认为在这里有这个解释很好,我确实接受了马特霍普的回答,因为它是第一个。不过,我仍然需要对此进行一些研究,因为我还认为我应该使用另一种类型(DTO)来参数化这个方法。

以上是关于当类型是 C# 9 记录时,FluentAssertions Should().BeEquivalentTo() 在微不足道的情况下失败,似乎将对象视为字符串的主要内容,如果未能解决你的问题,请参考以下文章

如何在 C# 9 中复制/克隆记录?

C# 9+ 中的 HttpClient 空警告

添加记录以从 C# 访问数据库

C# 9.0 新特性

当 regex101 匹配时,RegEx C# 不匹配

C# 9.0 中的新增功能