Osy#betaRun it on Osyrin: osyrin.com

Reference / Entity

Changes

<Entity>.Changes(id) → List<EntityChange>

The lightweight, ungated answer to "who changed this row, and when": field NAMES only, never values, newest first. Readable by anyone who may read the row itself — no `app.Audit` grant needed. For the actual old/new values, or a query spanning more than one row, see [[config-audit]]'s `EntityAuditRecord`.

stable2 examples compiled by CIentityauditobservability

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 first

Takes exactly one argument — the id of the row whose history you want. EntityChange carries:

FieldTypeMeaning
ByIdGuid?who made the change — null for a system or anonymous write
Bystringtheir display name, AS IT WAS at the time — empty when there was no principal
AtDateTimewhen the change was committed
Fieldsstring[]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#

Related

audit read access (app.Audit)

`app.Audit` configures the app's audit trails — WHO may read each one, and whether it is recorded at all. An app that…

ConcurrencyCheck

Plain optimistic concurrency: refuses an update whose value was changed by somebody else after the value the write was…

use

Declares a capability your app depends on, written inside the `app { }` manifest block. It provisions the capability…

Where / Single / Count

Query an entity by writing a predicate over it. The query runs in the database — not a filter over rows you already…