I started to use FluentAssertions recently, which supposed to have this powerful object graph comparison feature.
I'm trying to do the simplest thing imaginable: compare the properties of an Address
object with the properties of an AddressDto
object. They both contain 4 simple string properties: Country, City, Street, and ZipCode (it's not a production system).
Could someone explain to me, like I'm two years old, what is going wrong?
partnerDto.Address.Should().BeEquivalentTo(partner.Address)
And it fails with this message:
Message:
Expected result.Address to be 4 Some street, 12345 Toronto, Canada, but found AddressDto { Country = Canada, ZipCode = 12345, City = Toronto, Street = 4 Some street }.
With configuration:
- Use declared types and members
- Compare enums by value
- Match member by name (or throw)
- Without automatic conversion.
- Be strict about the order of items in byte arrays
It seems it tries to treat the Address
object as a string (because it overrides ToString()
?). I tried to use the options.ComparingByMembers<AddressDto>()
option, but seemingly it makes no difference.
(AddressDto
is a record
btw, not a class
, since I'm testing out new .Net 5 features with this project; but it probably makes no difference.)
Moral of the story:
Using record
instead of class
trips FluentAssertions, because records automatically override Equals()
in the background, and FluentAssertions assumes it should use Equals()
instead of property comparisons, because the overridden Equals()
is probably there to provide the required comparison.
But, in this case the default override implementation of Equals()
in a record
actually only works if the two types are the same, so it fails, and thus FluentAssertions reports a failure on BeEquivalentTo()
.
And, in the failure message FluentAssertions confusingly reports the issue by converting the objects to string via ToString(). This is because records have 'value semantics', so it treats them as such. There is an open issue about this on GitHub.
I confirmed that the problem does not occur if I change record
to class
.
(I personally think FluentAssertions should be ignoring Equals() override when it's on a record
and the two types are different, since this behavior is arguably not what people would expect. The current question, at the time of posting, pertains to FluentAssertions version 5.10.3.)
I edited my question title to better represent what the problem actually is, so it could be more useful for people.
References:
As people asked, here is the definition of the domain entity (had to remove some methods for brevity, since I'm doing DDD, but they were surely irrelevant to the question):
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}";
}
And here are the Dto equivalents:
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; }
}
}
Address
andAddressDto
? – SynonymizepartnerDto
variable? And how isAddress
property defined? – MallardPartnerDetailsDto
(record, not class) with properties equivalent to the ones defined in the domain entityPartner
. ThePartnerDetailsDto.Address
is of typeAddressDto
(its definition was added to the question), which is a public record defined insidePartnerDetailsDto
. – Giule