vkhorikov / CSharpFunctionalExtensions

Functional extensions for C#

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Entity mutable hashcode

leubac opened this issue · comments

The method Entity.GetHashCode() may return a different value along the entity lifecycle because the implementation is based on property Id.
The problem arises when some ORM (in my case NHibernate) will mutate this value when the object is being saved but was previously added to a persistent set (hashtable key is computed when object is inserted and never updated afterwards).

The problem is described in details here.

Here is the modification (BasicTests):

[Fact]
public void HashCode_stays_the_same_even_if_id_changes()
{
    var entity = new MyEntityChangingId();
    var hashCode1 = entity.GetHashCode();
    entity.SetId(1);
    var hashCode2 = entity.GetHashCode();

    hashCode1.Should().Be(hashCode2);
}

public class MyEntityChangingId : Entity
{
    public void SetId(long newId) => Id = newId;
}

and Entity:

private int? _oldHashCode;
public override int GetHashCode()
{
     if (_oldHashCode.HasValue)
         return _oldHashCode.Value;

     if (IsTransient())
     {
         _oldHashCode = base.GetHashCode();
         return _oldHashCode.Value;
     }
     return (ValueObject.GetUnproxiedType(this).ToString() + Id).GetHashCode();
}

PS: I would like to submit a PR but I'm unable to push my branch to the repository (having Git message Permission to vkhorikov/CSharpFunctionalExtensions.git denied to leubac).

I would like to submit a PR but I'm unable to push my branch to the repository (having Git message Permission to vkhorikov/CSharpFunctionalExtensions.git denied to leubac).

You need to fork this repo push your stuff into the forked repo and then you will be able to open a PR.

Apology for the late reply.

This looks good. Just a note: in my experience, this never led to issues. And if it does, you can change the way you assign Ids to entities (client Id generation instead of database/ORM-led one). But no harm in implementing this change, just to be safe.

The proposed implementation is a breaking change:

public void Test_HashSet()
{
	// Create a transient entity.
	var carEntity1 = Car.CreateNew();
	var h1 = carEntity1.GetHashCode();
	
	// Simulate saving the entity with ORM who is setting the Id through reflection(1 in this case).
	carEntity1.SetId(1);
	
	// Pass as illustrated in first post.
	h1.Should().Be(carEntity1.GetHashCode());
	
	// Creates a non transient entity with ID 1.
	var carEnttiy2WithSameIdThan1 = Car.Create(1);
	
	HashSet<Car> cars = new HashSet<Car>();	
	cars.Add(carEntity1);
	cars.Add(carEnttiy2WithSameIdThan1);
	
	// Fail: Expected collection to contain 1 item(s), but found 2.
	cars.Should().HaveCount(1);
}
cars.Should().HaveCount(1);

suceeds with current Entity's GetHashCode().
Any people who relies on collections that checks equality on insertion (for whatever reasons) will have bugs.

@leubac
Like said Vladimir you should consider generate your id's instead of relying on the database , problem solved.
If you are fine with Guid, well that's the easiest, otherwise you can have a look at HI/LO algorithm.

Ok it makes sense not to alter this library which is (almost) ORM agnostic with a change which is purely ORM induced.