Skip to content

What changed between two versions: Diff ​

Which fields did this edit change, and from what to what?

cs
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:

membervalue
Propertythe EF Core IProperty that changed: Property.Name, its CLR type, column name and annotations
Pathwhere the property sits, dot-separated: "Status", or "Address.City" for a member of a complex property or owned reference; use it as the label
OldValuethe value in the older version, or null when older is null
NewValuethe 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) for History<T> results. It checks that older starts before newer. 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: two AsOf results, two AllVersions rows, or a snapshot against the entity as it is now:
cs
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 first

A 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 null as older to diff the insert: every recorded property comes back, key included, with OldValue set to null.
  • Delete tombstones throw. The VersionOperation.Delete row that History<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 throws ArgumentException. Filter it out, as the first sample does, and read the delete from Operation.
  • 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 ​

situationexception
the two versions have different keysArgumentException
older does not start before newer (Version<T> overload), including the same version twiceArgumentException
either version is a delete tombstoneArgumentException
the entity type is not temporalInvalidOperationException
the entity has a recorded shadow property, such as a foreign key with no CLR propertyNotSupportedException

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.

Released under the MIT License.