Summary#
<Entity>.Changes(id) answers "who changed this row, when, and which fields" — the facts behind a concurrency
refusal, handed to your own code rather than only baked into the platform's own message. It carries field names
only, never values, which is what lets it be read by anyone who may read the row itself: a caseworker is not an
auditor, and this is not the audit trail.
For the full change record — the actual old/new values, or a query that spans more than one row — see
audit read access (app.Audit)'s EntityAuditRecord, which is deny-by-default and needs an app.Audit grant.
Signature#
<Entity>.Changes(<id>) → List<EntityChange> // newest firstTakes exactly one argument — the id of the row whose history you want. EntityChange carries:
| Field | Type | Meaning |
|---|---|---|
ById | Guid? | who made the change — null for a system or anonymous write |
By | string | their display name, AS IT WAS at the time — empty when there was no principal |
At | DateTime | when the change was committed |
Fields | string[] | the properties this change touched, by name — filtered to the ones the caller may read, so a masked column never appears even as a name |
Description#
Needs the capability, not a grant#
<Entity>.Changes(…) needs the Osysharp.Observability capability — using Osysharp.Observability; at the top of
the file — exactly like the audit-trail read surfaces in audit read access (app.Audit). What it does not need is anything from
app.Audit: there is no Read predicate to satisfy, because the answer carries no values, only field names, and a
reader who may see the row at all may see which of its fields last moved.
Why not just read EntityAuditRecord?#
You can — EntityAuditRecord.Where(e => e.EntityId == id) answers the same question with the actual values attached.
Changes exists because most callers do not want that: a concurrency refusal, a "last edited" badge, or a change
history strip on a form wants WHO and WHEN, not a values diff a caseworker was never granted to read. Reaching for
the full trail there would mean either widening the audit grant to people who are not auditors, or building a second,
values-stripped view yourself — Changes is that view, built in.
Examples#
using Osysharp.Observability;
entity Case {
[Required, MaxLength(40)] string Reference;
decimal? Amount;
}
List<string> HistoryLines(Case c) {
var lines = new List<string>();
foreach (var change in Case.Changes(c.Id)) {
var who = change.By == "" ? "somebody" : change.By;
var what = string.Join(", ", change.Fields);
lines.Add(who + " changed " + what);
}
return lines;
}using Osysharp.Observability;
entity Case {
[Required, MaxLength(40)] string Reference;
decimal? Amount;
}
string LastEditor(Guid id) {
var changes = Case.Changes(id);
return changes.Count == 0 ? "nobody yet" : changes[0].By; // newest first
}See also#
- audit read access (app.Audit) — the full trail (
EntityAuditRecord), its values, and who may read it - ConcurrencyCheck — the refusal this data backs, when two writers collide
- use — the
use/usingsplit that gates the capability