Osy#the first language built for agents
Agents firstAgentic appsWorkflowsDurable Execution — built inSecurityTestingThe editorThe UI modelOne program

Reference / Entity

Comparing entity rows

a == b — two entity references are equal when they are the SAME ROW

`==` on two entity references compares them by ROW IDENTITY — not by object reference, and not field by field. Two references to the same row are equal however you fetched them, so `list.Contains(row)`, `list.IndexOf(row)` and `list.Remove(row)` all find the row you mean. You never have to compare `.Id` yourself.

stable4 examples compiled by CIentityqueryauthoring

Summary#

Two references to the same row are the same row, whichever query each came from:

entity Contact {
  [Required] string Name;
  string Email;
}

bool OneRowTwoReads(string name) {
  var a = Contact.Single(c => c.Name == name);
  var b = Contact.Where(c => c.Name == name).First();
  return a == b;                   // true — two reads, one row
}

a and b were fetched by two separate queries and are still equal. You do not have to write a.Id == b.Id.

Signature#

a == b        // true when both references are the SAME ROW
a != b        // the negation
a == null     // no row — an unset reference, or a lookup that found nothing

Description#

An entity compares by identity, a class by value#

An entity is a row in a table. It has an identity of its own, that identity is what the platform stores, and it is what == asks about. So the rule cuts both ways:

  • The same row read twice is equal to itself — through two queries, in two orders, on the server or on the client. Nothing about how you got hold of it changes the answer.
  • Two different rows are never equal, however identical their columns. Two rows are two rows.
bool TwinsAreOneRow() {
  var twins = Contact.Where(c => c.Name == "Twin").OrderBy(c => c.Id).ToList();
  return twins[0] == twins[1];     // false — same name, same email, two rows
}

A class has no identity to compare, so its contents are the answer instead. That is the only difference between the two rules, and it follows from what each thing is.

Contains, IndexOf and Remove all find the row#

Every list operation that takes an element compares with the same ==, so over a list of rows each of them is asking "is this the same row?" — and each of them gets it right, including when the list came from one query and the row from another:

string Positions(Contact one) {
  var ordered = Contact.OrderBy(c => c.Name).ToList();
  return "at=" + ordered.IndexOf(one)              // its position, or -1
       + " has=" + ordered.Contains(one)           // is it in this list at all
       + " gone=" + ordered.Remove(one)            // take that row out of the list
       + " left=" + ordered.Count;
}

one was never read by the query that built ordered, and all three still identify it. The same holds on the client: a row a screen is holding — off a live var, or handed to an action by a foreach — is the same row the list holds, and these three operations say so.

FindIndex over .Id is not wrong, only longer#

This is the same question, asked the long way round:

bool BothWaysAgree(Contact one) {
  var ordered = Contact.OrderBy(c => c.Name).ToList();
  return ordered.FindIndex(c => c.Id == one.Id) == ordered.IndexOf(one);   // always true
}

If you have written the FindIndex form, nothing is broken — it returns the same index, and it is a perfectly readable thing to have written. It is simply not needed: IndexOf already compares by identity, so the lambda is restating the rule the language applies anyway.

Reach for FindIndex when you genuinely cannot hold the element — you have an id off a URL, a name typed by a user, or any other description of the row rather than the row. That is what it is for.

See also#

Related

entity

Declares a persisted type — a table of rows the app stores, queries and secures. Every entity gets an Id and audit…

Comparing classes

`==` on two class values compares them BY VALUE, field by field, rather than by reference. A class is a value you build…

LINQ over a local list

Query a local `List<T>`, `HashSet<T>` or `T[]` — of your own `class` values OR of plain scalars like `string[]` and…

Child collections (navigating a relation)

A parent's child collection — `order.Lines` — is not a loaded array. It is a QUERY, correlated to that parent, and…