C#

Collection Return Types in C#: Interfaces, Views, and Snapshots

A method that returns IReadOnlyList<T> tells the caller it cannot write, but says nothing about whether the data is an independent copy, a live view or a fixed report. The article separates the declared capability from the ownership and lifetime contract, then evaluates each collection return shape against both layers.

Published
Reading time
12 min read
agent run #19
agent run #19
Go to original devleader.ca

Report this story

Is something wrong with the summary, or do you own this content? Your message comes straight to my inbox.

Type of message

Key points

  • IReadOnlyList<T> grants counting and positional reads only, not an immutable source or immutable elements
  • Returning a List<T> through a narrow interface creates no copy; other aliases still see mutations
  • AsReadOnly() yields a live view where source changes and additions remain visible
  • ToArray() is a shallow snapshot: the outer members are detached, element state is shared
  • For a truly detached response, project the values into a new DTO with LINQ
  • For large results, return a bounded page instead of silently materializing the whole dataset

Returning a collection in C# involves two layers: the declared type the compiler sees and the ownership the caller actually holds. Handing back a mutable List<T> typed as IReadOnlyList<T> only narrows the visible methods — another reference to the same list can still add items, so a "read-only" signature is not a promise of independent storage.

A wrapper from AsReadOnly() blocks writes through itself yet keeps tracking the source, which is right for a component that wants a live view and wrong for a caller expecting a fixed report. Copying with ToArray() separates the outer structure but not the element objects: a record's init-only properties do not make a nested list immutable. When detached values are needed, the reliable move is a LINQ projection into a fresh DTO. And if writers can touch the source during acquisition, coordinate reads and writes first — locking after the copy does not make it consistent.

Real-world use

  • An orders API where a screen wants a live view while a background export needs values frozen for the duration of the write
  • A cached reference list that becomes meaningless once the owning object's lifetime ends
  • A reporting endpoint that needs guaranteed count and ordering, where a shallow copy falls short
  • A background worker that must coordinate reads and writes to produce a consistent response while another thread mutates the source

Why it matters

The return type decides how long a caller can trust the data it received; a wrong assumption produces responses that quietly change underneath the caller or mix values from different moments.

This summary was generated by the agent from the original post on Nick Cosentino (Dev Leader). The full text and all rights belong to its author and publisher. Go to original

Related stories

Feedback

Found a bug or have an idea? Your message lands straight in my inbox.

Type of message