What changed between two versions: Diff
Which fields did this edit change, and from what to what?
var versions = await db.History<Policy>()
.Where(v => v.Entity.Id == policyId && v.Operation != VersionOperation.Delete)
.OrderBy(v => v.ValidFrom) // oldest first: Diff takes the older version first
.ToListAsync();
Version<Policy>? previous = null;
foreach (var version in versions)
{
foreach (var change in db.Diff(previous, version))
{
Console.WriteLine(
$"{version.ValidFrom:u} {version.ChangedBy}: {change.Path} {change.OldValue} → {change.NewValue}");
}
previous = version;
}db.Diff(older, newer) compares two versions of the same entity and returns the properties whose values differ, each as a PropertyChange:
| member | value |
|---|---|
Property | the EF Core IProperty that changed: Property.Name, its CLR type, column name and annotations |
Path | where the property sits, dot-separated: "Status", or "Address.City" for a member of a complex property or owned reference; use it as the label |
OldValue | the value in the older version, or null when older is null |
NewValue | the value in the newer version |
Values are your entity's CLR values: an enum stored as text comes back as the enum. Who made the change and when is not repeated on a PropertyChange; read it from the Version<Policy> you passed in.
The diff runs in memory over versions you have already loaded. It sends no query.
Which properties are compared
Every property that history records, in model order (key first), then the properties of complex properties and owned references, reported one by one: when a member is set to null, each of its properties that had a value comes back with a null new value. A property you excluded is not in history, so it is never compared. Values are compared with each property's EF Core value comparer, the same one change tracking uses: arrays and collections compare by content, and a property with a value converter compares by its CLR value.
The result is empty when every recorded value is equal.
Versions and snapshots
There are two overloads:
Diff(Version<T>? older, Version<T> newer)forHistory<T>results. It checks thatolderstarts beforenewer.History<T>returns newest first, so order the versions oldest first (as above) or pass them in reverse.Diff(T? older, T newer)for plain snapshots: twoAsOfresults, twoAllVersionsrows, or a snapshot against the entity as it is now:
var atRenewal = await db.Policies.AsOf(renewalDate).SingleAsync(p => p.Id == policyId);
var now = await db.Policies.SingleAsync(p => p.Id == policyId);
var changedSinceRenewal = db.Diff(atRenewal, now); // older snapshot firstA plain entity has no period, so this overload cannot tell which snapshot is older. It reports older's values as old, whatever you pass.
The first version and deletes
- Against nothing. Pass
nullasolderto diff the insert: every recorded property comes back, key included, withOldValueset tonull. - Delete tombstones throw. The
VersionOperation.Deleterow thatHistory<T>returns records when and by whom a policy was deleted. Its values repeat the last version before the delete, so it is not a state to compare, and passing it on either side throwsArgumentException. Filter it out, as the first sample does, and read the delete fromOperation. - Re-created entities. If a policy is deleted and later inserted again with the same key, diffing the last version before the delete with the new insert compares the two states across the delete.
An empty diff from the interceptor writer
If EF Core marks a property as modified but its value did not change, the interceptor writer still records an update version, and diffing it against the previous version returns an empty list. The trigger writer records no version in that case. Skip empty results if your audit screen should not show them.
What throws
| situation | exception |
|---|---|
| the two versions have different keys | ArgumentException |
older does not start before newer (Version<T> overload), including the same version twice | ArgumentException |
| either version is a delete tombstone | ArgumentException |
| the entity type is not temporal | InvalidOperationException |
| the entity has a recorded shadow property, such as a foreign key with no CLR property | NotSupportedException |
History records shadow properties, but the snapshots that History<T>, AsOf and AllVersions return have no member to hold them, so a change to one would be invisible. Diff throws rather than return a diff that misses it. Map the property on the entity class to diff it.