Epiforge.Extensions.Expressions 4.4.0

There is a newer version of this package available.
See the version list below for details.
dotnet add package Epiforge.Extensions.Expressions --version 4.4.0
                    
NuGet\Install-Package Epiforge.Extensions.Expressions -Version 4.4.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Epiforge.Extensions.Expressions" Version="4.4.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Epiforge.Extensions.Expressions" Version="4.4.0" />
                    
Directory.Packages.props
<PackageReference Include="Epiforge.Extensions.Expressions" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Epiforge.Extensions.Expressions --version 4.4.0
                    
#r "nuget: Epiforge.Extensions.Expressions, 4.4.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Epiforge.Extensions.Expressions@4.4.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Epiforge.Extensions.Expressions&version=4.4.0
                    
Install as a Cake Addin
#tool nuget:?package=Epiforge.Extensions.Expressions&version=4.4.0
                    
Install as a Cake Tool

This library has useful tools for dealing with expressions:

  • ExpressionEqualityComparer - Defines methods to support the comparison of expression trees for equality
  • ExpressionExtensions, providing:
    • Duplicate - Duplicates the specified expression tree
    • SubstituteMethods - Recursively scans an expression tree to replace invocations of specific methods with replacement methods

Observable

This library accepts a LambdaExpression and arguments to pass to it, dissects the LambdaExpression's body, and hooks into change notification events for properties (INotifyPropertyChanged), collections (INotifyCollectionChanged), and dictionaries (Epiforge.Extensions.Collections.INotifyDictionaryChanged).

// Employee implements INotifyPropertyChanged
var elizabeth = Employee.GetByName("Elizabeth");
var observer = new ExpressionObserver();
var expr = observer.Observe(e => e.Name.Length, elizabeth);
// expr subscribed to elizabeth's PropertyChanged

Then, as changes involving any elements of the expression occur, a chain of automatic re-evaluation will get kicked off, possibly causing the observable expression's Evaluation property to change.

var elizabeth = Employee.GetByName("Elizabeth");
var observer = new ExpressionObserver();
var expr = observer.Observe(e => e.Name.Length, elizabeth);
// expr.Evaluation.Result == 9
elizabeth.Name = "Lizzy";
// expr.Evaluation.Result == 5

Also, since exceptions may be encountered after an observable expression was created due to subsequent element changes, observable expressions include a Fault property in their evaluations, which will be set to the exception that was encountered during evaluation.

var elizabeth = Employee.GetByName("Elizabeth");
var observer = new ExpressionObserver();
var expr = observer.Observe(e => e.Name.Length, elizabeth);
// expr.Evaluation.Fault is null
elizabeth.Name = null;
// expr.Evaluation.Fault is NullReferenceException

Observable expressions raise property change events of their own, so listen for those (kinda the whole point)!

var elizabeth = Employee.GetByName("Elizabeth");
var observer = new ExpressionObserver();
var expr = observer.Observe(e => e.Name.Length, elizabeth);
expr.PropertyChanged += (sender, e) =>
{
    if (e.PropertyName == "Evaluation")
    {
        var (fault, result) = expr.Evaluation;
        if (fault is not null)
        {
            // Whoops
        }
        else
        {
            // Do something with result
        }
    }
};

While an expression is working out its new value it can pass through results that were never simultaneously true of its inputs; an addition whose two operands both derive from the same property has to compute one of them before the other. You are not told about those. Every event you receive carries a value the expression genuinely held, so a subscriber that redraws or broadcasts on one does that work once rather than twice, the second time only to correct the first.

Nor are you told anything at all when a change leaves the value where it found it. That is decided by a comparison, using the same equality the expression uses everywhere else, and it happens before PropertyChanging rather than after — so a handler for that event still reads the previous value, and a pair of events always means the value really moved.

When you dispose of your observable expression, it will disconnect from all the events.

var elizabeth = Employee.GetByName("Elizabeth");
var observer = new ExpressionObserver();
using (var expr = observer.Observe(e => e.Name.Length, elizabeth))
{
    // expr subscribed to elizabeth's PropertyChanged
}
// expr unsubcribed from elizabeth's PropertyChanged

How an Expression Gets Observed

Observe takes a shortcut when it can and builds a graph when it cannot, deciding once when the observation is created. You receive the same values through the same events either way; the shortcut is just faster and lighter.

The shortcut handles an expression built from these:

  • the argument, constants, and captured locals
  • fields, on anything above — including static fields
  • properties and indexers whose target is one of the above
  • static properties
  • operators, where one resolved to a method needs a return type nothing could dispose — == on strings qualifies
  • method calls, on that same condition, where the target and every argument are themselves handled — string.IsNullOrEmpty(e.Name) qualifies

Everything else builds the graph: ?:, &&, || and ??; anything read through a property, such as e => e.Name.Length; a call to the get method of a property or an indexer, which is how an indexer written in C# arrives; object and collection construction; and anything you have configured the observer to ignore notifications for or to dispose.

To find out about a particular expression, ask:

var analysis = new DirectSubscriptionAnalyzer(options).Analyze(expression.Body);
// analysis.IsEligible is false
// analysis.Ineligibility is DirectSubscriptionIneligibility.DeferredBranch
// analysis.IneligibleExpression is the part responsible

Hand the analyzer the same options you hand the observer, since some of them decide what gets subscribed to at all. Set UseDirectSubscription to false if you would rather always have the graph; it is true by default.

Fields Are Read Once

Whatever a field held when an observation began is what that observation goes on using — a captured local, a field of your own class, and a static field alike. Assigning it afterward does not reach an observation that already exists. Static properties behave the same way, so e => e.Hired < DateTime.Now compares against the moment it was created for as long as it lives.

var threshold = low;
using var expr = observer.Observe(e => e.Salary > threshold.Amount, elizabeth);
threshold = high;    // expr is still comparing against low
low.Amount = 50000;  // expr re-evaluates
high.Amount = 90000; // expr does not

If you want the comparison to follow the value, do not assign the field — make the thing it points at a property of an object that notifies, and read that instead.

Observable expressions will also try to automatically dispose of disposable objects they create in the course of their evaluation when and where it makes sense. Use the ExpressionObserverOptions class for more direct control over this behavior. You can use the Optimizer property to specify an optimization method to invoke automatically during the observable expression creation process. We recommend Tuomas Hietanen's Linq.Expression.Optimizer, the utilization of which would look like so:

var options = new ExpressionObserverOptions { Optimizer = ExpressionOptimizer.tryVisit };

var a = Expression.Parameter(typeof(bool));
var b = Expression.Parameter(typeof(bool));

var lambda = Expression.Lambda<Func<bool, bool, bool>>
(
    Expression.AndAlso
    (
        Expression.Not(a),
        Expression.Not(b)
    ),
    a,
    b
); // lambda explicitly defined as (a, b) => !a && !b

var observer = new ExpressionObserver(options);
var expr = observer.Observe<bool>(lambda, false, false);
// optimizer has intervened and defined expr as (a, b) => !(a || b)
// (because Augustus De Morgan said they're essentially the same thing, but this involves less steps)

Observable Queries

This library provides re-implementations of LINQ operations, but instead of returning IEnumerable<T>s and simple values, these return IObservableCollectionQuery<T>s, IObservableDictionaryQuery<TKey, TValue>s, and IObservableScalarQuery<T>s. This is because, unlike traditional LINQ operations, these implementations continuously update their results until those results are disposed. What they hand back is a read-only view of the source: change the source, and the query brings itself up to date. Queries do not implement the mutating range collection and dictionary interfaces, because a query result is not somewhere you put things.

But... what could cause those updates?

  • the source is enumerable, implements INotifyCollectionChanged, and raises a CollectionChanged event
  • the source is a dictionary, implements Epiforge.Extensions.Collections.INotifyDictionaryChanged<TKey, TValue>, and raises a DictionaryChanged event
  • the elements in the enumerable (or the values in the dictionary) implement INotifyPropertyChanged and raise a PropertyChanged event
  • a reference enclosed by a selector or a predicate passed to the method implements INotifyCollectionChanged, Epiforge.Extensions.Collections.INotifyDictionaryChanged<TKey, TValue>, or INotifyPropertyChanged and raises one of their events

That last one might be a little surprising, but this is because all selectors and predicates passed to Observable Query methods become Observable Expressions (see above). This means that you will not be able to pass one that an ExpressionObserver cannot observe (e.g. a lambda expression that can't be converted to an expression tree or that contains nodes that are unsupported). But, in exchange for this, you get all kinds of notification plumbing that's just handled for you behind the scenes.

Suppose, for example, you're working on an app that displays a list of notes and you want the notes to be shown in descending order of when they were last edited.

var notes = new ObservableCollection<Note>();
var collectionObserver = new CollectionObserver();

var observedNotes = collectionObserver.ObserveReadOnlyList(notes);
var orderedNotes = observedNotes.ObserveOrderBy(note => note.LastEdited, isDescending: true);
notesViewControl.ItemsSource = orderedNotes;

From then on, as you add Notes to the notes observable collection, the IObservableCollectionQuery<Note> named orderedNotes will be kept ordered so that notesViewControl displays them in the preferred order.

Since IObservableCollectionQuery<T>'s are automatically subscribing to events for you, you do need to call Dispose on them when you don't need them any more.

void Page_Unload(object? sender, EventArgs e)
{
    orderedNotes.Dispose();
    observedNotes.Dispose();
}

Ahh, but what about exceptions? Well, Observable Expressions contain a Fault element in their Evaluation properties, but... you don't really see those Observable Expressions as an Observable Query caller, do ya? For that reason, Observable Queries all have OperationFault properties. You may subscribe to their PropertyChanging and PropertyChanged events to be notified when an Observable Expression or the overall Observable Query runs into a problem. If there is more than one fault in play, the value of OperationFault will be an AggregateException.

Dictionary queries adopt the key comparer of the dictionary they observe, discovering it through Epiforge.Extensions.Collections.Generic.IHashKeys<TKey> or a Dictionary<TKey, TValue>'s own Comparer, so a query over a case-insensitive dictionary is itself case-insensitive.

ObserveGroupBy, ObserveToLookup, and ObserveDistinct do not order their results the way LINQ does. Groupings are ordered by when they were created and the elements of a grouping by when they were added, rather than by where they occur in the source. This is deliberate: holding a grouping at the position of its key's first occurrence would mean moving that grouping every time an element was inserted ahead of it, announcing a change to something whose membership did not change, which is the opposite of what an Observable Query is for. Call ObserveOrderBy on the query, or on a grouping, when you want a defined order.

Reach for foreach rather than the indexer, because the difference between them is larger than it looks and grows with the collection. An enumeration takes the query's lock once and then walks a list, while the indexer takes that lock again for every element you ask for; on a large collection it must also find each one in a tree, because a query keeps its elements' positions in one so that a change repairs only what it touched. A query does remember the position it handed out last and searches outward from there, so asking for positions in order, or near one another, costs a fraction of asking for them at random, and what remains is mostly the repeated locking rather than the search. Walking ten thousand elements by index instead of by enumerator measured between thirty and fifty times slower in order, and around two hundred times out of order; at a hundred elements it was about fifteen, and there the repeated locking is the whole of it. Where you do need elements by position more than once, copy the query's contents and index the copy.

Since the ExpressionObserver has a number of options governing its behavior, you may optionally pass one you've made to the constructor of CollectionObserver to ensure those options are obeyed when Observable Expressions are created to enable your Observable Queries.

How Observable Queries Work and When to Use Them

It is worth being plain about what kind of thing this is, because "LINQ, but observable" undersells it and sets the wrong expectations.

A LINQ query is a description of a computation you run. Run it again and it does all of the work again. An Observable Query is not re-run. It is a small machine that holds the answer and repairs it, so when something changes, only the parts of the answer that depended on that thing are recomputed. The work is proportional to what changed rather than to how much data you have. If you want the name the literature uses for this idea, it is incremental, or self-adjusting, computation.

Three things that might otherwise look like arbitrary restrictions fall straight out of that:

  1. Your selectors and predicates have to be expression trees rather than delegates because the machine has to read them to find out what they depend on. A delegate is opaque; there is nothing in it to subscribe to.
  2. You have to dispose of a query because it is holding subscriptions to everything it depends on, and those subscriptions are the entire reason the answer stays right.
  3. Faults reach you through OperationFault instead of being thrown, because the evaluation that failed happened later, on whatever thread raised the change. By then there is no call of yours left on the stack to throw out of.

What is not free is construction. Building the machine means building an observable expression for every element the query touches, and that is proportional to the size of the collection. So build a query once and hold onto it. Do not build one per frame, per request, or per keystroke. The bargain is that you pay up front and then stop paying to read.

Reading is also cheaper than being told. A query subscribes to the one it is built on only while something is subscribed to it, and a filtered query works out where a change landed, and describes it, only when something will receive that description. So subscribe when you need to be told what changed, and simply read the query when you only need its answer to be right.

Which is also how to decide whether you want one. If you compute a result once and move on, plain LINQ is cheaper and simpler, and you should use it. If a result has to stay correct across a long run of small changes, such as a list someone is looking at, a running total, or a filter someone is typing into, that is what these are for.

Choosing Between Observable Queries and DynamicData

DynamicData is the nearest thing to this in .NET, and it is a good library. Both keep a derived collection correct as your data changes — filter, sort, group, project, aggregate — and both update the result when an element's property changes rather than only when the collection does. Here is how to tell which one you want.

Start with what you already know. If you know INotifyPropertyChanged, ObservableCollection<T> and LINQ, this library asks you to learn almost nothing else: you point it at the collection you already have, write ObserveWhere(person => person.Rank > 0), and bind the result. If you already know Rx, or you use ReactiveUI, DynamicData will feel like home and this library will feel like an unfamiliar dialect — and it is probably already somewhere in your dependency graph. Most of the rest follows from that one answer.

What you will actually run into This library DynamicData
Where your data lives The ObservableCollection<T> you already have A SourceCache or SourceList; adapting an existing collection is possible but much slower
Saying which property to watch Read out of your expression You name it with AutoRefresh — forget it and your view goes quietly stale
A property change that does not change the result Costs nothing Materializes a change set each time
When your projection throws A fault you can bind to; the query keeps working Ends the subscription, as Rx does, unless you use TransformSafe
Combining collections ObserveConcat, chained Also union, intersection, difference, and merging a changing set of sources
Showing only what is on screen A fixed slice that stays correct Live paging and virtualization driven by a stream of requests
Composing with anything else reactive Not applicable Everything in Rx composes with it
An expression it cannot analyze Falls back to a slower path, says so in your log, results unchanged Not applicable

Use DynamicData if you are already in Rx; you need to combine several collections by set operations; you need live paging, virtualization, size limits or expiry; you need asynchronous projections; or you want the reassurance of a large and long-established user base.

Use this library if you want a live view of a collection you already have, with the least new vocabulary, and you care about what an individual property change costs.

What It Costs

These are from the benchmarks in this repository, against DynamicData 9.4.33 at a thousand elements unless stated otherwise. Each propagation figure is per property change, above what the same changes cost with nothing observing them at all.

This library DynamicData
A property change that does not alter a filtered view 0 B, 7.3 ns 608 B, 192.4 ns
An element changing group 578 B, 236.9 ns 1,891 B, 604.8 ns
An element moving in a sorted view 292 B, 1,259.5 ns 414 B, 984.4 ns
Building a filtered view 965 KB, 294 μs 4,119 KB, 2,169 μs
What a live filtered view holds 934 B per element 1,865 B per element

The zero is exact rather than rounded: a property change that does not move an element in or out of a filtered view allocates nothing here, at a thousand, ten thousand and a hundred thousand elements alike. This library re-evaluates the predicate in place and stays silent when the answer has not moved; DynamicData's model is a stream of change sets, so a refresh has to materialize one. Neither is a defect. One library pays per change and the other pays per change that matters.

Two of those rows move with the size of the view, in opposite directions, and this is the part worth reading twice.

  • Sorting. DynamicData's cost per move grows with the collection while this library's barely does, so the two cross at about 1,400 elements. Below that DynamicData is 1.28x faster; at four thousand this library is 1.72x faster and at ten thousand 2.98x.
  • Grouping. The reverse. DynamicData's cost per migration is flat while this library's grows, so the two cross at about 7,900 elements. Below that this library is 2.55x faster; at ten thousand DynamicData is 1.18x faster — though it holds about twice the memory to do it, 1,954 B per element against 1,024.

The grouping crossover is a trade rather than an oversight, and knowing which side of it you want is more useful than the number. A grouping here keeps its elements in the order they were added, so moving one out of its old group means finding it first, which is work proportional to the size of that group. DynamicData's groups are keyed rather than positional, so a removal is a dictionary operation and costs the same whatever the group holds. If you need the elements of a group in a stable order, that is what you are paying for. If you do not, DynamicData's shape is cheaper once groups get large. A lookup built with ObserveToLookup is the same shape as a grouping here and behaves the same way.

What decides both is the size of the view the operator sees, not the size of your collection. Filter ten thousand elements down to a thousand and then sort, and you are on the small-view side of the sorting crossover, where DynamicData wins; grouping that same thousand puts you well on this library's side of the grouping one.

Allocation does not cross. At every size measured, this library allocates less for the same work: nothing at all for a filtered view, about a third of DynamicData's for grouping, about seven tenths for sorting.

Composition behaves. Ordering or grouping a filtered view costs each library close to the sum of its parts rather than more, so a chain does not change which one to prefer — only the size of the view arriving at each stage does.

Two more things worth knowing before you weigh any of the above.

A live view is not free in either library. One over ten thousand elements holds about 9 MB here and about 19 MB in DynamicData, against 960 KB for the elements themselves. Building a view is likewise proportional to the size of the collection in both. Build one and keep it; neither library rewards building views casually.

ToObservableChangeSet() over an existing ObservableCollection<T> costs DynamicData about 210x what its own SourceCache does for the same property changes. That is the path you land on if you adopt it without changing where your data lives, and it is worth knowing about before you do.

These comparisons were written by someone who does not use DynamicData, which is a real limitation on them. The harness is in this repository, the workloads are ordinary ones, and corrections are welcome.

Product Compatible and additional computed target framework versions.
.NET net6.0 is compatible.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 is compatible.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 is compatible.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
5.0.0 52 9/10/2026
4.4.0 45 9/9/2026
4.3.0 93 9/2/2026
4.2.0 88 8/31/2026
4.1.0 84 8/31/2026
4.0.0 113 8/30/2026
3.0.1 100 8/29/2026
3.0.0 189 8/27/2026
2.3.8 224 6/6/2026
2.3.7 123 6/6/2026
2.3.6 130 6/6/2026
2.3.5 122 6/3/2026
2.3.4 128 5/31/2026
2.3.3 122 5/29/2026
2.3.2 133 5/8/2026
2.3.1 124 5/7/2026
2.3.0 125 5/7/2026
2.2.0 133 4/23/2026
2.1.2 132 4/14/2026
2.1.1 144 4/12/2026
Loading failed

Observing an expression whose result is a boolean no longer allocates a box for that result on every evaluation. The two boxes a boolean can need are now shared across every observation, in both the graph and the direct-subscription path.
Observing an expression also allocates less to construct. Each node held its own box for the default value of its type; those boxes are now shared, except where the type is one the observer could be asked to dispose of.
A query no longer allocates when it hears that an element's or a key's value has changed and no fault has changed with it, which is the ordinary case. An element crossing a filtered query's predicate now costs the query nothing at all where nothing is subscribed to it, and only the event arguments a subscriber receives where something is.
Observing a method call, an indexer, a constructor, an invocation, a member initialization or an array initializer allocates substantially less on every evaluation, because reading its operands' faults and values no longer builds a lazy sequence to do it. A method call observed through the expression graph now allocates under 50% of what it did.
A dictionary query no longer produces the change notifications nobody has subscribed to. A value crossing a filtered dictionary query's predicate now costs that query nothing beyond observing the predicate itself where nothing is subscribed to it, which is 1/7 of what it cost and 50% of the time. Subscribing to one of a dictionary query's change events no longer causes the other two to be produced as well, so a query with a subscriber allocates under 50% of what it did. Filtered and projected dictionary queries alike benefit; a projected dictionary query with nothing subscribed to it now allocates under 25% of what it did, in 50% of the time.
A query no longer adapts the selectors and predicates it is given by wrapping them in an invocation, which put an extra node in every observation for the query to build when an element arrives and to walk on every evaluation. This applies to filtered, projected and to-collection dictionary queries and to ordered and grouped collection queries. Observing an element which has just arrived allocates around 25% less, and propagating a change is 10% to 25% faster.
An observed indexer over a dictionary no longer allocates anything of its own when the dictionary changes. Finding the changed key no longer builds a closure and two delegates per change, so an observed indexer now costs exactly what any other subscriber to that dictionary costs and nothing more.
Fixed a defect in which an observed indexer did not see a change to the object it indexes when that object announced the change the conventional way, by raising a property change notification naming its indexer followed by empty brackets. Only the indexer's bare name was recognized. Types which also raise a collection or dictionary change notification were unaffected, because that notification carried the same change.
An observation whose result is a value type which does not implement IEquatable<T> — the key-value pair a projection produces, for instance — no longer allocates when it checks whether that result has changed. The check received the result already boxed and unboxed it, only for the comparer it reached to box it again. Such an observation now allocates ninety-six bytes less every time it is re-evaluated, 37% less in all, and costs exactly what an observation of an equatable result of the same shape costs.
An expression which reads a property through another property, such as the length of a name, can now be observed by subscribing directly to its change sources, provided nothing the intermediate value could hold raises a change notification of its own. Reading the length of a string is the common case. A filtered query over a thousand elements built from such a predicate allocates 36% of what it did.
An expression using &&, || or ?? can now be observed by subscribing directly to its change sources, provided the operand whose evaluation is deferred reaches nothing the rest of the expression does not already watch — which is the case whenever the operands read from the same objects, as a predicate testing two properties of the same element does. Such a predicate had been the most expensive shape a caller could write; a filtered query over a thousand elements built from one now allocates 24% of what it did and builds in 6.6% of the time.
Observing a method call, an indexer or a constructor no longer builds an argument array on every evaluation where the member takes two arguments or fewer, and neither those nor a property read look up how to invoke the member on every evaluation any more: the invocation is resolved once when the observation is constructed and the operands' values are handed to it directly. Per change, a method call returning an integer now allocates 96 bytes rather than 128 and takes 21% less time, a projection producing a key-value pair allocates 120 rather than 160 and takes 15% less, and a property read, which never built an array, takes 11% less.
An expression which reads an element of a collection or a dictionary by its indexer can now be observed by subscribing directly to its change sources. Every indexer read had been refused, because the compiler emits it as a call to the indexer's get method and that shape went unrecognized. Reading a key of an observable dictionary, over a thousand elements, now allocates 1.29 megabytes rather than 15.24 and takes 4.8% of the time; reading an index of an observable list costs the same, and of a plain list, 1.22 megabytes rather than 4.39. This had been the most expensive shape a caller could write.
Fixed a defect in which an observed indexer read through the expression graph reported what the dictionary stored rather than what the indexer returns. It answered from the change notification it received instead of evaluating the expression, which is the same thing only where the indexer is a plain lookup, and an observable dictionary's indexer is virtual. An observation over a dictionary whose indexer had been overridden reported the wrong value with no fault raised and nothing a consumer could detect. Removing a key likewise produced an error of the observer's own manufacture rather than the one the indexer raises, and announced the removal twice. Both now evaluate the expression, and a removal is announced once.
Fixed a defect in which an observation through the expression graph could announce a value composed of inputs which were never simultaneously current. Each node subscribed to the object it reads separately, so an object which changed two of its properties and announced them with a single notification drove one propagation per interested node, and the first of those announced the new value of one property combined with the stale value of the other. Every node reading from the same object is now told together, within one propagation, and only the settled result is announced.
An observation through the expression graph allocates far less to construct where several of its nodes read from the same object. Each node attached its own handler to that object's notification, and every attachment reallocates the notification's invocation list, so the cost grew with the square of the number of nodes sharing it: a thousand observed indexes into one collection allocated 15.24 megabytes and now allocate 4.79, in 55% less time. There is one handler per object and event now, which costs 160 bytes for each distinct object observed, so an observation whose nodes share nothing pays around 6% more to construct than it did.
An expression using &&, || or ?? whose deferred operand reads from an object the rest of the expression does not touch, and an expression using the conditional operator whatever its branches read, can now be observed by subscribing directly to their change sources. The subscriptions belonging to such an operand are attached the first time an evaluation reaches it, which is where the expression graph attaches them and after which neither mechanism lets them go, so the two agree on what is subscribed at every point rather than only at the end.
Observing an expression by subscribing directly to its change sources allocates less to construct, whatever the expression is. The array holding an observation's subscriptions was made at the size of every source the analysis named and then copied to the size actually needed; it is now made once at that size. A query over a thousand elements allocates 0.04 to 0.06 megabytes less to build, which is 4% of what a simple comparison predicate costs and 4.4% of what an indexer read costs.
An observation no longer subscribes more than once to the same event of the same object. An expression which names one member in several places — the test of a conditional and its branches, an indexer and the collection it indexes, or simply the same property read twice — planned a subscription for each place and attached every one of them, at the cost of an object to hold each and a whole redundant evaluation every time that object announced a change. A query over a thousand elements built from a conditional whose branches read what its test reads now allocates 1.14 megabytes to construct rather than 1.28, in 78% of the time, and evaluates once per change where it evaluated three times. This holds however the object is named: two reads of a captured variable or a static field are two separate expressions which name one object, and they are now recognized as one subscription rather than two.
An expression which reads a property through another property whose value can itself raise change notifications — the rank of a person's partner, say — can now be observed by subscribing directly to its change sources. The observation follows the intermediate: it attaches to whatever that property holds, and when the value is replaced it releases the object it left and attaches to the new one, which is what the expression graph has always done. A query over a thousand elements built from such a predicate now allocates 1.56 megabytes to construct rather than 3.74, in 15% of the time.
Fixed a defect in which the two ways of observing an expression could evaluate different expressions. Where an optimizer is configured, it rewrites what the graph observes but was not applied to expressions observed by subscribing directly to their change sources, so an operand the optimizer removed was evaluated by one and not the other — which is visible when that operand would throw. Both now observe the same expression, and observing without optimization is unaffected.
A source an observation subscribes to no longer carries a lock of its own. Its list of attachments is only ever changed while the registry holding every source is itself locked, so the second lock guarded nothing and cost an object for each source an observation subscribes to. A query over a thousand elements whose predicate reads a property of each of them allocates 48 kilobytes less to build.
An observation no longer builds the list of the arguments it was made with unless something asks for one. Every observation carried that list whether or not anyone read it, at 24 bytes apiece, which a query over a thousand elements paid a thousand times. The list is now made on first request and kept, so it is the same list every time it is read, and it costs nothing at all where it is never read — which is every observation the library makes for itself.
A query which observes an expression for each of its elements no longer makes a new event handler for each one. Attaching to an observation's change notification converted a method group, which yields a fresh delegate every time it is converted, and detaching converted it again, so a query over a thousand elements built and discarded two thousand delegates where one would have served. Every collection and dictionary query which observes per element now keeps a single handler and attaches that. A filtered query over a thousand elements allocates 0.12 megabytes less to build and tear down, which is 11% of what it cost; projecting, and projecting a dictionary to a collection, are improved the same way.
An ordered query no longer walks the change it is told about with a query of its own. Both handlers a key change wakes — the one which repositions the element and the one which keeps its keys in order — used a query to search a list of one selection and to take one element out of a payload of one item, which allocated six objects for every key change for the life of the query. Both now walk by index. Changing one key in a query of a thousand elements ordered on an observed property allocates 408 bytes rather than 776, which is 47% less, in 7% less time; where the change reorders nothing it allocates 248 rather than 616, which is 60% less, in 25% less time.
An expression which reads more than one property of the same object no longer places a handler on that object for each of them. Observing by subscribing directly to change sources kept one registration per property name, where the expression graph has always kept one per object and event and decided relevance when the change arrives; the direct path now does the same, and each subscription is asked whether it wants the name reported before anything is evaluated. A change to a property no subscription named still begins no propagation. Building a query over a thousand elements whose predicate reads two properties of each of them allocates 1035 kilobytes rather than 1325, the second property having cost 360 bytes per element and now costing 72, and every change to such an element propagates 15% faster, since one handler is invoked where two were. Where an expression reads one property of an object of its own, building it now costs 8 bytes per element less; where it reads a property of an object shared by every element, 8 bytes more.
Adding an element to a query which orders its source, or removing one, no longer groups the change it is told about. Four places grouped the elements a change carried so that one element carried twice would be counted once, which builds a lookup, its array of groups, a group and an array of its one element every time — and a change which adds or removes a single element, which is most of them, can carry no repeat at all. Each of the four now handles a single element directly and groups only what can hold a repeat. Adding one element to an ordered query of a thousand and removing it again allocates 5261 bytes rather than 6704, which is 21% less, in 10% less time.
An expression which constructs an object can now be observed by subscribing directly to its change sources, where before it was refused and observed by building a graph instead. This is what every query which orders, groups, or looks up by an observed key is built on: each of them projects its elements to a pair of the element and its key, and that projection constructs the pair, so none of them had ever taken the faster path. A value whose type can be disposed is still refused, since the expression observer may be told to dispose what an expression constructed. Adding one element to a query of a thousand ordered by an observed property and removing it again allocates 2024 bytes rather than 5261, which is 61% less, in 67% less time, and now costs the same whether the query holds a hundred elements or ten thousand; changing one key allocates 360 bytes rather than 408. The same change to a query of a thousand grouped by an observed property allocates 1864 bytes rather than 5213, which is 64% less, in 19% of the time.
Where an expression cannot be observed by subscribing directly to its change sources, the observer now says so to the logger at debug level, naming the expression, the part of it which is not eligible, and why. Nothing about which expressions qualify has changed; what could previously be discovered only by comparing what an observation allocates against what it ought to allocate is now stated outright. The report is made where the analysis happens rather than where the expression is observed, so a query which observes one predicate for each of a thousand elements is told once and not a thousand times.
Repositioning an element of an ordered query is 4% to 7% faster. The binary search which finds where the element now belongs asked for the comparable keys of the element it is moving at every probe, though they cannot change while a search is running, and reached every element it compares against by descending from the root of the sequence of positions rather than from the element it had just read. Changing one key in a query of a thousand elements ordered on an observed property now takes 1,280 nanoseconds rather than 1,338, and moving an element thirty-two positions takes 1,330 rather than 1,423. Allocation is exactly what it was.