SO shows possible duplicates for my question, but I'm not quite sure my case (and actually the question itself) is the same.
I have an entity with complex property, let's say
public class Entity
{
public Guid Id {get;}
public ComplexObject Complex {get;}
}
public class ComplexObject
{
public object CurrentObject{get;}
public IReadOnlyCollection<object> PendingObjects{get;}
}
Just inb4 I have a DDD approach so this limits me to have setters private, so as I got change tracker could use only 'snapshot' strategy for storing/comparing values.\
The issue is here: I'm getting entity, do some operation with ComplexObject that modifies it's internal state, then on commiting changes to database EF doesn't sends updated value for Complex (as I understood) because Change Tracker returned this property haven't changed.
I've reproduced this on simple example:
- Read entity from database:
DbContext.Entry(entity).Property(x=>x.Complex)...OriginalValue/CurrentValuereturns empty object{CurrentObject = null, PendingObjects = []}as it should be in my case.\ - I do some update operation that should set something into
CurrentObject(and optionally intoPendingObjectsbut it doesn't matter)\ - Calling
Updatemethod for repository infrastructure in my code that actually does not much but at least I could intercept this operation and check DbContext.DbContext.Entry(entity).Property(x=>x.Complex)...OriginalValue/CurrentValuereturns here equivalent state like{CurrentObject = {...}, PendingObjects = []}however Original one should differ.
I was managed to fix this issue by re-setting value for entity's Complex property each time some operation happens (immutable type approach). That lead me to a thought something is wrong with comparing instances. Implementing IComaprable<>, IEquatable<> or ICloneable for ComplexObject did nothing for me (perhaps I did something wrong). I also tried to implement ICloneable for Entity thinking that EF's snapshot taker will try to clone object first, but code haven't entered any of these methods.
In general this fix is quite a workaround I would like to use if any other option will fail.
Also a case googled on SO was to enforce Change Tracker saying that properties A, B, C are changed (DbContext.Entry(entity).Property(x=>x.Complex).IsModified = true) well, but this is a shot into my leg.
So the question: is there any docs or article saying how snapshot is being taken from entity by EF and is there a chance to intercept it somehow (or at least try to help it detect changes for properties I know changed).
P.S. Not sure if it will help but some updated circumstances. Complex Property field is JSON-serializable into database (Postgres actually). Perhaps there will be a thoughts to use some cool value comparer or whatever component, but in fact I'll compare same referenced instance with same referenced instance, so this seems not a case, or again I'm doing something wrong but not sure what exactly.
P.P.S. Okay, let me show you some code in case you want :)
internal abstract class AsynchronouslyUpdatedAggregate : AggregateRoot
{
public ProcessSemaphore ProcessSemaphore { get; }
...
internal void ReleaseLock<TAggregate>(AsynchronousProcess process)
where TAggregate : AsynchronouslyUpdatedAggregate
{
var releaseLock = ProcessSemaphore.TryReleaseLock(process);
if (!releaseLock.IsLockRemoved)
{
throw new AggregateLockedByAnotherProcess(Id, ProcessSemaphore.CurrentProcess?.Id, process.Id);
}
... // nothing interesting there. Semaphore object already modified or exception was thrown and any action doesn't make sense
}
}
internal class ProcessSemaphore
{
public Queue<AsynchronousProcess> PendingProcesses { get; }
public AsynchronousProcess? CurrentProcess { get; private set; }
...
public AggregateUnlockResult TryReleaseLock(AsynchronousProcess process)
{
if (CurrentProcess != process)
{
return new(false, null); // if this is a case, exception will be thrown, so SaveChanges method will never be called in my case
}
PendingProcesses.TryDequeue(out var newProcess);
CurrentProcess = newProcess; // <-- this is my 100% change
return new(true, newProcess);
}
}
internal record AsynchronousProcess(Guid Id);
And my test case where ProcessSemaphore property isn't registered as updated:
- Insert Entity inherited from
AsynchronouslyUpdatedAggregatehavingProcessSemaphore.CurrentProcessnot null - Call API that leads to
TryReleaseLockmethod or throws exception - ??? PROFIT
- SaveChanges called under the hood