Osy#betaRun it on Osyrin: osyrin.com

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 its row#

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 compares by identity too — the same instance — and a record by its contents. The entity's identity is its row, which is why the same row read twice is still one value.

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 from the…

Comparing classes

`==` on two `class` values asks whether they are the SAME INSTANCE, exactly as C# does: two objects built separately…

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…