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

Reference / Class

Sequence fields on a class

class <Name> { public <T>[] <Field>; // fixed-size public List<<T>> <Field>; // growable public HashSet<<T>> <Field>; // distinct, unordered public Dictionary<<K>, <V>> <Field>; }

A class can hold many values in one field — `string[]`, `int[]`, `List<Tag>`, `HashSet<string>`, `Dictionary<string, int>`. The element may be a scalar or another class, the whole LINQ surface reads it (a map through `.Keys` / `.Values`), and an array is fixed-size exactly as in C#. An entity cannot hold one: a collection member of an entity is a relation.

stable6 examples compiled by CIclasscollectionstypes

Summary#

A class field can hold many values. Four spellings, all of them C#'s:

class Tag { public string Name; }

class Document {
  public string Title;
  public string[] Keywords;             // fixed-size — its length is part of it
  public int[] Scores;                  // any scalar, not just string
  public List<Tag> Tags;                // growable
  public HashSet<string> Seen;          // distinct, unordered
  public Dictionary<string, int> Counts;
}

Signature#

public T[] Field;                      // array   — fixed-size; indexable, .Count, foreach, LINQ. No .Add
public List<T> Field;                  // list    — everything an array does, plus .Add / .Remove / .RemoveAll
public HashSet<T> Field;               // set     — distinct membership; .Add / .Contains / .Remove
public Dictionary<K, V> Field;         // map     — d[k], .ContainsKey, .Add(k, v), .Remove(k), .Keys, .Values

T may be a scalar (string, int, long, decimal, double, bool, Guid, DateTime, …) or another class. byte[] is the exception and is not a sequence at all — it is the binary scalar type.

Description#

Reading one#

Every sequence field answers the same questions, and they are the LINQ verbs you already use — filter, project, sort, aggregate — evaluated in memory over the values the object holds:

int LongKeywords(Document d) {
  return d.Keywords.Where(k => k.Length > 5).Count;
}

string FirstAlphabetically(Document d) {
  return d.Keywords.OrderBy(k => k).First(k => k != "");
}

bool EveryScorePasses(Document d) {
  return d.Scores.All(s => s >= 0);
}

// `Max` has no zero to fall back on, so over NO scores it answers absent — which is why this returns `int?`
// where `Count` above returns `int`. Writing `int` here is a compile error, not a silent 0.
int? Best(Document d) {
  return d.Scores.Max(s => s);
}

Indexing and foreach read it directly, with no ceremony:

int Total(Document d) {
  var sum = 0;
  foreach (var s in d.Scores) { sum = sum + s; }
  return sum + d.Scores[0];
}

Filling one#

A collection literal fills any of them at construction:

Document New() {
  return new Document {
    Title = "notes",
    Keywords = ["osy", "reference"],
    Scores = [10, 30, 20]
  };
}

A map's contents#

A Dictionary<K, V> answers d[k], .ContainsKey(k), .Add(k, v), .Remove(k) and .Count. .Keys and .Values hand you its contents as an ordinary sequence — which is what lets every verb above reach a map at all:

int TotalSeen(Document d) {
  return d.Counts.Values.Sum();
}

int HowManyKeys(Document d) {
  return d.Counts.Keys.Count;
}

int Busiest(Document d) {
  return d.Counts.Values.Where(c => c > 1).Count;
}

Both are a snapshot, not C#'s live view: a .Values you already took does not follow a later .Add. Take it again when you want the current contents.

Changing one#

A List<T> grows and shrinks with .Add(x) and .Remove(x). .RemoveAll(x => …) deletes every match in one go and answers how many went — it changes the list you called it on, so anything else holding that same list sees the change too:

int DropShortTags(Document d) {
  return d.Tags.RemoveAll(t => t.Name.Length < 3);
}

That in-place mutation is the difference from d.Tags.Where(…), which answers a new sequence and leaves the original alone. Reach for Where when you want a filtered view, and RemoveAll when the list itself should change.

To ask where something sits rather than change anything, .IndexOf(item), .LastIndexOf(item) and .FindIndex(x => …) answer its position, or -1 when it is not there. IndexOf finds the first occurrence and LastIndexOf the last, which is the only thing they disagree about. Over a list of entity rows all three compare by row identity, so .IndexOf(row) finds the row without your comparing .Id.

Because it changes things, a change belongs in an action (or an on-change body), never in a render slot — a render expression is re-evaluated whenever anything it reads changes, in an order nobody controls, so the compiler refuses .Add / .Remove / .RemoveAll there and names where they go instead. The reads (.Count, .Contains, .Keys, .Values, indexing, every LINQ verb) are render-slot material and always were.

Array or list?#

Use a List<T> when the contents change — it is what you will want most of the time. Use a T[] when the set of values is settled once and then only read.

The difference is C#'s and the compiler holds you to it: an array's length is part of the type, so it has no .Add:

d.Keywords.Add("x");   // ✗ `Add` needs a growable collection, and `string[]` is fixed-size
d.Tags.Add(tag);       // ✓ a List grows

Nothing else differs. Both are indexable, both count, both foreach, both answer every LINQ verb — so choosing an array never costs you a way to read it.

Can an entity hold a list?#

This is a class surface, and deliberately. On an entity a collection member means something else entirely: OrderLine[] Lines is a relation — a set of child ROWS, stored in their own table and reached through a foreign key. A column cannot hold a list, so string[] Tags; on an entity is refused, and it names the two real answers:

  • Model each value as a row. A DocumentTag entity holding one string, reached as [ForeignKey(Document)] DocumentTag[] Tags; — which is queryable, indexable, and the answer whenever you will ever want to search or count by it.
  • Or keep the list on a class, when it is a value the row simply carries and nobody queries across.

See relations for the relation form.

See also#

  • LINQ over a local list — the verbs that read these fields, and the same verbs over a local sequence
  • class properties — a member that runs a body on access
  • relations — what a collection member means on an entity, and why it is a different thing

Related

class properties

A class member that reads and writes like a field but runs a body on access — a computed value, a validating setter, or…

Classes

A class is an in-memory shape — data plus the behaviour that belongs to it — and it never touches the database. That is…

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…

relations

One entity points at another by declaring it as a member — that is the foreign key. The parent reads its children back…