ReadOnlyCollection and ReadOnlyDictionary in C

In C#, ReadOnlyCollection<T> and ReadOnlyDictionary<TKey, TValue> provide read-only wrappers around mutable collections to prevent unauthorized modifications while exposing internal data. This article explains what these classes are, why and when to use them, how they differ from immutable collections, and provides straightforward code examples demonstrating their implementation.

What Are ReadOnlyCollection and ReadOnlyDictionary?

Located in the System.Collections.ObjectModel namespace, ReadOnlyCollection<T> and ReadOnlyDictionary<TKey, TValue> are wrapper classes that implement read-only interfaces (IReadOnlyList<T>, IReadOnlyCollection<T>, and IReadOnlyDictionary<TKey, TValue>).

Rather than copying data into a new data structure, these wrappers hold a reference to an existing mutable collection (IList<T> or IDictionary<TKey, TValue>). They expose members for reading data—such as indexers, count properties, and enumeration—while omitting or explicitly disabling methods that modify state, such as Add, Remove, or Clear.

Primary Use Cases

1. Encapsulation and Data Protection

The primary use case is exposing internal state from a class without allowing consumers to alter the underlying collection. This preserves the integrity of your domain models and services.

using System.Collections.Generic;
using System.Collections.ObjectModel;

public class Order
{
    private readonly List<string> _items = new();

    // Expose as ReadOnlyCollection
    public ReadOnlyCollection<string> Items { get; }

    public Order()
    {
        Items = new ReadOnlyCollection<string>(_items);
        // Alternatively using LINQ / extension method:
        // Items = _items.AsReadOnly();
    }

    public void AddItem(string item)
    {
        _items.Add(item);
    }
}

Consumers can read the Items property and iterate through it, but they cannot invoke .Add() or .Remove() directly on the property.

2. Communicating API Intent

Using these types in method signatures or property definitions explicitly signals to other developers that the collection is intended for reading only. This eliminates ambiguity compared to exposing IList<T> or List<T>.

3. Exposing Key-Value Lookups Safely

ReadOnlyDictionary<TKey, TValue> serves the same purpose for dictionary structures. It allows consumers to query values by key without granting the ability to insert, overwrite, or remove entries.

using System.Collections.Generic;
using System.Collections.ObjectModel;

public class ConfigurationManager
{
    private readonly Dictionary<string, string> _settings = new();

    public ReadOnlyDictionary<string, string> Settings { get; }

    public ConfigurationManager()
    {
        Settings = new ReadOnlyDictionary<string, string>(_settings);
        // Or using extension method:
        // Settings = _settings.AsReadOnly();
    }

    public void SetSetting(string key, string value)
    {
        _settings[key] = value;
    }
}

Key Characteristics and Behaviors

  • Live View of Data: Because these classes wrap existing instances rather than creating defensive copies, any changes made to the underlying mutable collection internally will immediately reflect in the read-only wrapper.
  • Low Memory Overhead: Creating a wrapper does not duplicate the elements in memory, making it a lightweight operation compared to cloning or copying collections.
  • Not Intrinsically Thread-Safe: If the underlying collection is modified on one thread while another thread is reading from the read-only view, thread safety issues can still occur.

ReadOnly vs. Immutable Collections

It is important not to confuse ReadOnlyCollection<T> with types from the System.Collections.Immutable namespace (such as ImmutableList<T> or ImmutableDictionary<TKey, TValue>):

  • ReadOnly Collections: Act as a "window" or view over a mutable collection. The data can change if the underlying collection is modified.
  • Immutable Collections: Cannot be modified once created. Any operation that appears to mutate an immutable collection returns an entirely new instance, leaving the original intact.