Let's say I have a type structure like this:
class Address
{
public string Id { get; set; }
protected bool Equals(Address other) => Id == other.Id;
public override int GetHashCode() => Id.GetHashCode();
public override bool Equals(object? obj)
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
if (obj.GetType() != this.GetType()) return false;
return Equals((Address)obj);
}
}
class PrivateAddress : Address
{
public string Street { get; set; }
}
record Person
{
public string Name { get; set; }
public Address Address { get; set; }
}
And with this structure, I have these two persons:
[Test]
public void TestPerson()
{
var person1 = new Person
{
Name = "Bob",
Address = new PrivateAddress
{
Id = "1",
Street = "1st Street"
}
};
var person2 = new Person
{
Name = "Bob",
Address = new PrivateAddress
{
Id = "1",
Street = "2nd Street"
}
};
}
They are obviously not equal, the streets are different. I want my tests to reflect this.
I tried DeepEquals:
person1.ShouldDeepEqual(person2);
And FluentAssertions:
person1.Should().BeEquivalentTo(person2);
Both do not work (the tests succeed instead of failing). I assume because they use only Equals()
for the comparison of the addresses. But its correct that these types only get compared by ID, so I can't change that only to make the tests work.
Is there a way to compare these objects property by property recursively?
CodePudding user response:
It is not very clear what exact behavior you are after.
Your current implementation only uses the Id for equality. Therefor the two addresses are considered equal even if the streets are different. So I'm not sure why you saying that the two addresses are obviously not equal, you just defined them as being equal. You might want consider if you want to allow a PrivateAddress to be equal to a Address if they share the same Id.
If you want value equality the two addresses are unequal since they have different streets, to get this behavior you need to override the equality methods in the PrivateAddress class. Another way to get value equality would be to use a record or struct instead of a class, since these have value equality by default.
If you want reference equality you should not override any equals methods, since it is the default behavior for classes. You could also call ReferenceEquals
to explicitly check for reference equality.
If you want to use different types of equality in different circumstances you should use the IEqualityComparer<T>
interface and provide different comparers for different use cases. Methods that need to compare objects should take a IEqualityComparer parameter. So you can have one implementation that only uses the ID for equality, and one that compares all properties.
You might also consider implementing IEquatable<T>
since this provides typed equality methods.
CodePudding user response:
The problem here is that you have defined Equals
on Address
to be bool Equals(Address other) => Id == other.Id;
. This means that any object that is an Address
is equal to the current Address
if, and only if, the Ids match.
You can't later change your mind if the object is an address of a different subclass.
And what if we had this:
var person1 = new Person
{
Name = "Bob",
Address = new PrivateAddress
{
Id = "1",
Street = "1st Street"
}
};
var person2 = new Person
{
Name = "Bob",
Address = new Address
{
Id = "1",
}
};
What should happen now? Are they equal?
In any case, you should implement the standard Equals
correctly to try to avoid this situation.
Try this:
class Address
{
public string Id { get; init; } = "";
public override bool Equals(object? obj)
{
if (obj is Address other)
{
return this.Id == other.Id;
}
return false;
}
public override int GetHashCode()
{
return this.Id.GetHashCode();
}
}
class PrivateAddress : Address
{
public override bool Equals(object? obj)
{
if (obj is PrivateAddress other)
{
return base.Equals(obj) && this.Street == other.Street;
}
return false;
}
public override int GetHashCode()
{
return base.GetHashCode() ^ this.Street.GetHashCode();
}
public string Street { get; init; } = "";
}
Now, if you do that your test fails, as you wanted.
The only issue remaining is if your first address is Address
and the second is PrivateAddress
- this will pass. If you swapped them around then they would fail. Your problem is the substitution principle. Any Address
should work the same way, you're changing that way, and that breaks the priciple.
CodePudding user response:
Could you use an extension method like this, it will compare each property of two objects and check if they are equal?
public static bool EqualsRecursive(this T a, T b)
{
foreach (var propa in a.GetType().GetProperties())
{
var propb = b.GetType(). GetProperties().Where(x => x.Name == propa.Name).Single();
if (propb.GetValue(b, null) != propa.GetValue(a, null))
return false;
}
return true;
}
I guess you could then use the extension method on the comparison between propa
and propb
to recursively drill down the nested properties.
CodePudding user response:
You have a GetHashCode
method override which uses the id property which is 1
in both cases. Change the value of the Id
property from either one of the two items and the test will not likely pass.