Open-source project
Cysharp/ObservableCollections avatar
Cysharp/ObservableCollections

ObservableCollections: generic change notifications without the boxing in ObservableCollection

High performance observable collections and synchronized views, for WPF, Blazor, Unity.

1,023 stars73 forksC#MIT

At a glance

What is it?
ObservableCollections is a MIT licensed C# library that replaces the non-generic INotifyCollectionChanged pipeline with a generic, range-aware, thread-safe notification model, and adds synchronized views that hold transformed copies of the data. It targets WPF, Blazor and Unity, and it deliberately does not implement INotifyCollectionChanged itself.
Who is it for?
ObservableCollections is the right pick for a C# application whose list changes are the hot path and where the allocations in ObservableCollection show up in a profile, since the generic event args, the AddRange path and the synchronized view remove real work from the notification pipeline, and the MIT licence makes it easy to adopt; it is the wrong pick for an XAML application that binds directly to a collection, because the library does not implement INotifyCollectionChanged and you must call ToNotifyCollectionChanged() first, so before you migrate you should confirm that the extra transform copy in a view is acceptable for your data sizes and that your consumers can live with a collection that raises its own event type.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 17 days ago.
What is it written in?
Mainly C#, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

Why the built-in ObservableCollection is a performance problem

The argument this library makes is about the cost of the standard notification pipeline, and it is specific rather than ideological.

ObservableCollection is built on INotifyCollectionChanged, NotifyCollectionChangedEventHandler and NotifyCollectionChangedEventArgs, and the README names the consequences. There are no generics, so everything is boxed and memory is allocated on every notification. NotifyCollectionChangedEventArgs holds all its values in an IList even when the change is a single item, which causes further allocations. There is no range feature, so adding many values at once is reported as a stream of single-value notifications, and a lot of wastage occurs. And it is not thread-safe, which makes it hard to combine with the notifier.

The library's answer is a parallel set of types: a generic NotifyCollectionChangedEventHandler<T> and a generic NotifyCollectionChangedEventArgs<T>, using recent C# features such as in parameters, readonly ref struct and ReadOnlySpan<T>.

csharp
public delegate void NotifyCollectionChangedEventHandler<T>(in NotifyCollectionChangedEventArgs<T> e);

public readonly ref struct NotifyCollectionChangedEventArgs<T>
{
    public readonly NotifyCollectionChangedAction Action;
    public readonly bool IsSingleItem;
    public readonly T NewItem;
    public readonly T OldItem;
    public readonly ReadOnlySpan<T> NewItems;
    public readonly ReadOnlySpan<T> OldItems;
    public readonly int NewStartingIndex;
    public readonly int OldStartingIndex;
    public readonly SortOperation<T> SortOperation;
}

The IsSingleItem flag plus the two span fields is the design worth noting. A single-item change is passed as one value with no list allocation, and a range change is passed as a span over the existing data rather than a copied list. Sort and Reverse are now notified as well, which the standard collection does not report.

IObservableCollection and the thread-safety guarantee

Beyond the argument about allocation, the library asks you to use a different interface entirely. Instead of subscribing to INotifyCollectionChanged, you work with IObservableCollection<T>, and the stated guarantee is that this is thread-safe and can produce a view that is fully synchronized with the collection.

csharp
public interface IObservableCollection<T> : IReadOnlyCollection<T>
{
    event NotifyCollectionChangedEventHandler<T>? CollectionChanged;
    object SyncRoot { get; }
    ISynchronizedView<T, TView> CreateView<TView>(Func<T, TView> transform);
}

The interface extends IReadOnlyCollection<T>, so the surface it exposes is read-only enumeration and counting plus the change event, the sync root and view creation. That shape is what makes the thread-safety claim coherent: the collection is a write model that observers read, and the sync root gives a consumer the lock object used to coordinate a read with a concurrent write.

The observable collections in the library are ObservableList, ObservableDictionary, ObservableHashSet, ObservableQueue, ObservableStack, ObservableRingBuffer and ObservableFixedSizeRingBuffer, and each of them implements this interface. The event signature is the same generic delegate in every case, so a consumer that handles collection changes does not need to know which kind of collection it is attached to.

The trade-off is that writing to these collections is done through their own APIs rather than through the familiar ICollection surface, which means existing code that assumed a standard collection interface has to be revisited.

Synchronized views keep transformed copies of the data

The second half of the library is the synchronized view, and it exists to separate the model from the view model. The stated pattern is to use ObservableCollections as the Model and generate a SynchronizedView as the View (ViewModel), and the point is that this works for WPF, Blazor and Unity rather than only for one UI framework.

The view is created with a transform function and holds the transformed values itself:

csharp
var list = new ObservableList<int>();
var view = list.CreateView(x => x.ToString() + "$");

list.Add(10);
list.Add(20);
list.AddRange(new[] { 30, 40, 50 });
list[1] = 60;
list.RemoveAt(3);

foreach (var v in view)
{
    // 10$, 60$, 30$, 50$
    Console.WriteLine(v);
}

// Dispose view is unsubscribe collection changed event.
view.Dispose();

Two properties make this useful. The transform function is called only once during Add, so costly objects that are linked can be instantiated without being rebuilt on every notification. And the view can dynamically show or hide values using filters, so a view is not merely a copy but a projection.

The cost is equally clear. The view holds a second copy of the data, transformed, which means memory roughly doubles for that data set and every mutation is applied twice, once to the model and once to the view. For a log window or a chat transcript this is a good trade. For a collection with a hundred thousand items it is not.

Disposing the view unsubscribes the collection changed event, and the example calls that out, which is the one piece of lifetime management a caller must not forget.

The ring buffers exist for rotating log windows

Beyond the familiar list, dictionary, hash set, queue and stack, the library ships two ring buffer types: ObservableRingBuffer and ObservableFixedSizeRingBuffer.

The use case given is rotation, with the example of displaying up to 1000 logs where old ones are deleted when new ones are added. That is a genuinely common need in a diagnostics or activity view, and the standard collection types make it awkward, because removing from the front of an ObservableList is an O(n) operation and raises a removal notification.

The fixed size variant has an important property that its name implies: it drops the oldest entry when a new one arrives, so the collection never grows past its capacity and the memory profile is bounded no matter how long the process runs. The unbounded variant still rotates but keeps whatever it has been given.

The same section notes that AddRange allows for efficient batch processing of large numbers of additions, which ties back to the range notification support in the event args. A single AddRange call produces one notification carrying a span, rather than N notifications each carrying a boxed item, and for a bulk import that is the difference between a visible pause and no pause.

So the collection family is not padding. Each type exists because a specific data pattern, ordered access, lookup, membership, queue semantics, stack semantics, or a rotating window, needs it, and each one raises the same generic event.

Observing changes as R3 observables

For projects that use reactive programming, the library integrates with R3, the reactive extensions library, through an Observe extension.

csharp
Observable<CollectionChangedEvent<T>> IObservableCollection<T>.ObserveChanged()
Observable<CollectionAddEvent<T>> IObservableCollection<T>.ObserveAdd()
Observable<CollectionRemoveEvent<T>> IObservableCollection<T>.ObserveRemove()
Observable<CollectionReplaceEvent<T>> IObservableCollection<T>.ObserveReplace()
Observable<CollectionMoveEvent<T>> IObservableCollection<T>.ObserveMove()
Observable<CollectionResetEvent<T>> IObservableCollection<T>.ObserveReset()
Observable<Unit> IObservableCollection<T>.ObserveClear<T>()
Observable<(int Index, int Count)> IObservableCollection<T>.ObserveReverse<T>()
Observable<(int Index, int Count, IComparer<T>? Comparer)> IObservableCollection<T>.ObserveSort<T>()
Observable<int> IObservableCollection<T>.ObserveCountChanged<T>()

The split into per-action observables is the useful part. A consumer that only cares about additions subscribes to ObserveAdd and does not have to write a switch over an action enum, and the count change is available on its own without tracking the collection size by hand. Sort and Reverse surface the index and count they affected, and the sort also carries the comparer, which is what you need to reproduce the ordering in a parallel structure.

The event names map one to one onto the actions in the change event args, so the reactive surface is a projection of the same event model rather than a second, parallel abstraction. R3 is a dependency for these extensions, and the README positions the integration as the answer for teams who want to handle each change event with Rx style operators rather than a switch statement.

XAML binding, ToNotifyCollectionChanged, and the design boundary

There is a constraint in this library that catches people out, and the project states it directly: Observable Collections themselves do not implement INotifyCollectionChanged, so they cannot be bound on XAML platforms and the like.

The way around it is a conversion method, ToNotifyCollectionChanged(), which produces a collection that does implement the interface and is therefore suitable for binding. The cost is that you now maintain two objects, the observable collection and its adapter, and the adapter has to be kept alive for as long as the binding is active.

This is the sharpest design trade-off in the library. The performance argument against the standard pipeline only holds if you stay on the generic event path. Any time you convert back to the standard interface to satisfy a XAML binding, the generic advantage for that consumer is gone, and the notification types change shape underneath you.

The interface list is short enough to hold in mind. An application that is mostly WPF binding to collections gets less out of this library than one that is a Blazor or Unity application, or a desktop application that maintains its own view model layer and wants the transform-once behaviour of a synchronized view. The library is built for the second case and the trade-off is deliberate, not an oversight.

Adding the package itself is a single command for.NET, with NuGet as the source, and the README says to read the Unity section for that platform.

Licensing, release cadence, and what to check before upgrading

The licence is MIT, with a LICENSE file at the repository root, and the project is not archived with the last push on 2026-10-06, so the code is being worked on. The release history tells a different story about packaging: the most recent tagged version is v3.3.4 from 2025-07-04, preceded by v3.3.3 in December 2024 and v3.3.2 in November 2024. A year between the last release and the last commit is normal for a library where work happens on the main branch and the next version is cut when a set of changes is ready, but it means the version you install from NuGet may be older than the source on GitHub.

The repository layout is consistent with a serious library project. The solution is an ObservableCollections.slnx file, the new XML solution format, with Directory.Build.props carrying shared build properties, an opensource.snk strong name key, src/ and tests/ directories, a sandbox/ project for experiments, and a docs/ directory. That layout tells you the assembly is signed, which matters for anyone integrating it into a signed application build.

Before upgrading from a standard collection, the practical checks are concrete. Confirm that your consumers can work with a collection that does not implement INotifyCollectionChanged, or plan where ToNotifyCollectionChanged() goes. Confirm that the extra copy in a synchronized view fits your memory budget. And confirm that the transform in a view is genuinely expensive enough to be worth caching, since for cheap transforms the view is pure overhead.

Editorial conclusion

ObservableCollections is the right pick for a C# application whose list changes are the hot path and where the allocations in ObservableCollection show up in a profile, since the generic event args, the AddRange path and the synchronized view remove real work from the notification pipeline, and the MIT licence makes it easy to adopt; it is the wrong pick for an XAML application that binds directly to a collection, because the library does not implement INotifyCollectionChanged and you must call ToNotifyCollectionChanged() first, so before you migrate you should confirm that the extra transform copy in a view is acceptable for your data sizes and that your consumers can live with a collection that raises its own event type.

Frequently asked questions

How do I install ObservableCollections in a .NET project?

The library is distributed on NuGet, and the command given is dotnet add package ObservableCollections. For Unity the project points to a separate Unity section.

What is the difference between ObservableCollection and ObservableList in C#?

The built-in ObservableCollection raises non-generic INotifyCollectionChanged events, which boxes values, allocates for every notification, has no range support, and is not thread-safe. ObservableList raises a generic NotifyCollectionChangedEventHandler<T> with an event args type that carries a single item or a ReadOnlySpan for ranges, and it implements IObservableCollection<T>, which is documented as thread-safe.

Can I bind an ObservableCollections collection to XAML?

Not directly. The library states that its collections do not implement INotifyCollectionChanged, so they cannot be bound on XAML platforms. You can convert them with ToNotifyCollectionChanged(), which produces a collection suitable for binding.

What does a synchronized view do in ObservableCollections?

A view holds its own transformed copy of the collection, and the transform function runs only once during Add rather than on every notification. Views can also filter values dynamically, and disposing a view unsubscribes it from the collection changed event.

Which collection types does ObservableCollections provide?

ObservableList, ObservableDictionary, ObservableHashSet, ObservableQueue, ObservableStack, ObservableRingBuffer and ObservableFixedSizeRingBuffer. The fixed size ring buffer is described as the efficient option for rotating views such as showing the latest 1000 log lines, where old entries are dropped as new ones arrive.

Official sources

  1. Cysharp/ObservableCollections on GitHub
  2. Issues
  3. License: MIT
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/cysharp-observablecollections.svg)](https://hysenlabs.com/projects/cysharp-observablecollections)

Was this useful?