DEV Community

Cover image for Introduction to Dynamic Data
Nick Polyak
Nick Polyak

Posted on • Edited on

3 3

Introduction to Dynamic Data

What is DynamicData for?

DynamicData allows monitoring source collections and updating target collection in such a way that the updated collections satisfy certain conditions specified by the DynamicData operators.

The great feature of DynamicData is that the target collections pick up the source collection inserts, removes and updates and arrange the corresponding changes in the target collections to satisfy the same conditions.

This makes DynamicData very useful especially on the front end, allowing to manipulate the collections of non-visual objects, while the visual objects will be passively reflecting the changes.

Dynamic Data has been built by Roland Pheasant (apparently around 2014) by applying Reactive Extensions to monitor and update collections and since then consistently maintained and improved by him.

Image description

Image description

To the best of my knowledge, unlike Reactive Streams, DynamicData exists only in .NET universe (has not been translated to other languages yet).

Source Code Location

All the non-visual source code for this article is located under Intro to Dynamic Data project under NP.Samples repository. Clone or download the repository and start IntroToDynamicData.sln in Visual Studio 2022 to run the samples.

The samples are written as XUnit fixtures - for more on XUnit read Everything you need to know to create XUnit C# Tests in Visual Studio.

All samples for this article test methods (marked by [Fact] attribute) within SimpleExamples.cs file.

Introductory Sample

Our first sample demonstrates maintaining an output (target) collection of integers mimicking the input (source) collection, while including only even numbers, sorting them in descending order and taking their squares.

Our first example in located in file SimpleExamples.cs, test method SimpleExamples.IntTransformFilteringAndSorting().

Do not try to understand the whole code at once since I'll describe it almost line by line below.

[Fact]
public static void IntTransformFilteringAndSorting()
{
    // create source collection and populate
    // it with numbers 1 to 6.
    ObservableCollection<int> sourceInts = 
        new ObservableCollection<int>(Enumerable.Range(1,6));

    // create stream of IChange<int> parameters
    // from the source collection
    IObservable<IChangeSet<int>> changeSetStream = 
        sourceInts.ToObservableChangeSet();

    // create the target collection
    IObservableCollection<int> targetInts = 
        new ObservableCollectionExtended<int>();

    IObservable<IChangeSet<int>> resultObservable =
        changeSetStream
            // filter only even ints
            .Filter(i => i % 2 == 0)
            // sort descending
            .Sort(SortExpressionComparer<int>.Descending(i => i))
            // transform to squares
            .Transform(i => i * i)
            // bind the target collection to update
            .Bind(targetInts);

    // now subscribe to start pulling data
    // using clause will dispose the subscription
    using IDisposable subscribeDisposable =
        resultObservable.Subscribe();

    // make sure the target collection is correct
    Assert.True(targetInts.SequenceEqual([6 * 6, 4 * 4, 2 * 2]));

    // remove 2 (even int) from source collection
    sourceInts.Remove(2);

    // make sure the target collection is correct
    Assert.True(targetInts.SequenceEqual([6 * 6, 4 * 4]));


    // insert 3 integers 20, 21 and 22 in the
    // source collection. Notice that only even numbers
    // 20 and 22 will influence the 
    // target collection.
    sourceInts.Insert(0, 20);
    sourceInts.Insert(1, 21);
    sourceInts.Insert(2, 22);

    // make sure the target collection is correct
    Assert.True(targetInts.SequenceEqual([22 * 22, 20 * 20, 6 * 6, 4 * 4]));

    // empty out the source collection
    sourceInts.Clear();

    // check that the target collection
    // has also been emptied out
    Assert.Empty(sourceInts);
}
Enter fullscreen mode Exit fullscreen mode

First, we create a source collection and populate it with numbers 1 to 6:

ObservableCollection<int> sourceInts = 
    new ObservableCollection<int>(Enumerable.Range(1,6));
Enter fullscreen mode Exit fullscreen mode

Type ObservableCollection<T> is part of C# language, not specific to DynamicData. It implements INotifyCollectionChanged and fires CollectionChanged event whose arguments contain information about the changes to the collection. From this information, DynamicData creates an observable stream of changes to the collection by calling ToObservableChangeSet() extension method:

IObservable<IChangeSet<int>> changeSetStream = 
     sourceInts.ToObservableChangeSet();
Enter fullscreen mode Exit fullscreen mode

Then we define and create the target collection targetInts:

IObservableCollection<int> targetInts = 
    new ObservableCollectionExtended<int>();
Enter fullscreen mode Exit fullscreen mode

ObservableCollectionExtended<T> is a DynamicData type - an improvement over ObservableCollection<T> allowing more control over when CollectionChanged events are fired.

Now we create an observable of collection changes we want by manipulating the input changeSetStream with DynamicData operators:

IObservable<IChangeSet<int>> resultObservable =
    changeSetStream
        // filter only even ints
        .Filter(i => i % 2 == 0)
        // sort descending
        .Sort(SortExpressionComparer<int>.Descending(i => i))
        // transform to squares
        .Transform(i => i * i)
        // bind the target collection to update
        .Bind(targetInts);
Enter fullscreen mode Exit fullscreen mode

Note the DynamicData operators are named differently than LINQ and Rx operators. This is done on purpose to prevent the mix-ups with the corresponding Rx operators.

In particular

  1. Filter(...) stands for Where(...)
  2. Sort(...) is used instead of OrderBy(...)
  3. Transform(...) instead of Select(...)

The 3 operators mean that we filter in only even integers, sort them by descending order and take their squares as outputs.

The most interesting is the operator Bind(...) to which you pass the output collection. It binds the target collection to update in compliance with the operators performed on the source collection changes once the subscription is made.

Note: nothing will happen until we call Subscribe() on the resulting IObservable<IChangeSet<int>> - similar to IObservable<T> in Rx.NET:

using IDisposable subscribeDisposable =
    resultObservable.Subscribe();
Enter fullscreen mode Exit fullscreen mode

For the rest of the method, we use Assert.True(...) statements to make sure that the target collection's content is what we expect to be while modifying the source collection.

Dynamic Filter Demo

Our next small demo will show how the filter can be changed dynamically with the change being picked up immediately by the Dynamic Data. The test is located within the same file - SimpleExamples.cs and the test is a static method SimpleExamples.DynamicFilterTest():

[Fact]
public static void DynamicFilterTest()
{
    // create source collection and populate
    // it with numbers 1 to 6.
    ObservableCollection<int> sourceInts =
        new ObservableCollection<int>(Enumerable.Range(1, 6));

    // create stream of IChange<int> parameters
    // from the source collection
    IObservable<IChangeSet<int>> changeSetStream =
        sourceInts.ToObservableChangeSet();

    // create the target collection
    IObservableCollection<int> targetInts =
        new ObservableCollectionExtended<int>();

    Subject<Func<int, bool>> filter = 
        new Subject<Func<int, bool>>();

    IObservable<IChangeSet<int>> resultObservable =
        changeSetStream
            // filter only even ints
            .Filter(filter)
            // sort descending
            .Sort(SortExpressionComparer<int>.Descending(i => i))
            // transform to squares
            .Transform(i => i * i)
            // bind the target collection to update
            .Bind(targetInts);

    // now subscribe to start pulling data
    // using clause will dispose the subscription
    using IDisposable subscribeDisposable =
        resultObservable.Subscribe();

    // add filter only after the subscription
    // filter in only even numbers
    filter.OnNext(i => i % 2 == 0);

    // make sure the target collection is correct
    Assert.True(targetInts.SequenceEqual([6 * 6, 4 * 4, 2 * 2]));

    // filter in only number divisible by 3
    filter.OnNext(i => i % 3 == 0);

    // make sure the target collection is correct
    Assert.True(targetInts.SequenceEqual([6 * 6, 3 * 3]));

    // add a value 9 (divisible by 3) to the source sequence.
    sourceInts.Add(9);

    Assert.True(targetInts.SequenceEqual([9 * 9, 6 * 6, 3 * 3]));
}
Enter fullscreen mode Exit fullscreen mode

The test method similar to the previous one, so I'll be concentrating only on the differences.

In previous test method SimpleExamples.IntTransformFilteringAndSorting(), we passed the lambda expression of type Func<int, bool> to the Filter(...) method:

changeSetStream
    // filter only even ints
    .Filter(i => i % 2 == 0)
    ...
Enter fullscreen mode Exit fullscreen mode

to filter in only even numbers.

Now, instead of a Func<int, bool> lambda expression, we shall pass IObservable<Func<int, bool>> - so that we could replace the filtering lambda expression at will.

To make it simple (changing the filtering lambda expression) we shall define the filter IObservable<Func<int, bool>> as Subject<Func<int, bool>> (remember from the previous article that subjects allow us not only to subscribe to Rx Streams, but also put the new data into them):

Subject<Func<int, bool>> filter = 
    new Subject<Func<int, bool>>();

IObservable<IChangeSet<int>> resultObservable =
    changeSetStream
        // filter only even ints
        .Filter(filter)
        ...
Enter fullscreen mode Exit fullscreen mode

Note, that we only should start putting data into the filter after we subscribe to the collection changes (under the covers it also subscribes to the filter changes):

// now subscribe to start pulling data
// using clause will dispose the subscription
using IDisposable subscribeDisposable =
    resultObservable.Subscribe();

// add filter only after the subscription
// filter in only even numbers
filter.OnNext(i => i % 2 == 0);

// make sure the target collection is correct
Assert.True(targetInts.SequenceEqual([6 * 6, 4 * 4, 2 * 2]));
Enter fullscreen mode Exit fullscreen mode

Next, let us change the filter - to filter in numbers divisible by 3 (and not even numbers as before):

// filter in only number divisible by 3
filter.OnNext(i => i % 3 == 0);

// make sure the target collection is correct
Assert.True(targetInts.SequenceEqual([6 * 6, 3 * 3]));
Enter fullscreen mode Exit fullscreen mode

We immediately verify that the target sequence now contains square(6) and square(3).

Now you can play with adding and removing integers to and from the source and make sure that the target collection reflects the source changes.

Total Sum Aggregation Tests

Next we shall learn about summing up all numbers within a collection using Sum(...) operator. Note, there are several other aggregation operators built into Dynamic Data including Avg(...), Maxumum(...), Count() and Minimum(...) - they are self explanatory and all work in the same fashion as Sum(...).

The method demonstrating Sum(...) aggregation is SimpleExamples.TotalSubAggregationTest():

[Fact]
public static void TotalSumAggregationTest()
{
    // create source collection and populate
    // it with numbers 1 to 3.
    ObservableCollection<int> sourceInts =
        new ObservableCollection<int>(Enumerable.Range(1, 3));

    // create stream of IChange<int> parameters
    // from the source collection
    IObservable<IChangeSet<int>> changeSetStream =
        sourceInts.ToObservableChangeSet();

    // create the collection sum observable
    IObservable<int> sumObservable = changeSetStream.Sum(i => i);

    // create a variable to hold the sum
    int sumResult = 0;

    // subscribe to changes in total sum
    // first time it fires after the subscription.
    using IDisposable disposableSubscription =
        sumObservable.Subscribe(result => sumResult = result);

    // 1 + 2 + 3 = 6
    Assert.Equal(6, sumResult);

    // add some numbers
    sourceInts.AddRange([5, 10, 15]);

    // 1 + 2 + 3 + 5 + 10 + 15 = 36
    Assert.Equal(36, sumResult);

    // remove all numbers
    sourceInts.Clear();

    // test that we obtain 0 as a result
    // of removing everything
    Assert.Equal(0, sumResult);
}
Enter fullscreen mode Exit fullscreen mode

Note, that for the ease of figuring out the sum, I put only 1, 2 and 3 into the source collection, not numbers from 1 to 6 as in the previous samples.

Essentially the Sum(...) operator we use, transforms the IObservable<IChangeSet<int>> into IObservable<int> which represents the Rx stream of changes to the sum of all numbers within the collection:

IObservable<int> sumObservable = changeSetStream.Sum(i => i);
Enter fullscreen mode Exit fullscreen mode

Every time the input collection changes, the sumObservable Rx stream emits the new sum of all its numbers.

Grouping Sample

Our next sample shows a grouping of a collection of integers by the remainder from the division by 2 (even numbers vs odd numbers):

[Fact]
public static void GroupingTest()
{
    // create source collection and populate
    // it with numbers 1, 3 and 5 (at this point only odd numbers).
    ObservableCollection<int> sourceInts =
        new ObservableCollection<int>([1, 3, 5]);

    // create stream of IChange<int> parameters
    // from the source collection
    IObservable<IChangeSet<int>> changeSetStream =
        sourceInts.ToObservableChangeSet();

    // group the numbers using GroupWithImmutableState operator
    // by the remainder from division by 2 (odd vs even)
    IObservable<IChangeSet<DD.List.IGrouping<int, int>>> 
        groupedObservable =
            changeSetStream.GroupWithImmutableState(i => i % 2);

    // create the grouped collection
    using IObservableList<DD.List.IGrouping<int, int>> 
        groupedCollection =
            groupedObservable
                .AsObservableList();

    // grouped collection only has one group of odd numbers
    // since there are only odd number in the source collection:
    Assert.True(groupedCollection.Count == 1);

    // get odd group
    DD.List.IGrouping<int, int> oddGroup =
        groupedCollection.Items.Single(grouping => grouping.Key == 1);

    Assert.True(oddGroup.Items.SequenceEqual([1, 3, 5]));

    // add even numbers 2, 4, 6 to the source collection
    sourceInts.AddRange([2, 4, 6]);

    // now there should be two groups of numbers - odd and even
    Assert.True(groupedCollection.Count == 2);

    // get odd group
    oddGroup = 
        groupedCollection.Items.Single(grouping => grouping.Key == 1);

    // check that the numbers in the odd group are [1, 3, 5]
    Assert.True(oddGroup.Items.SequenceEqual([1, 3, 5]));

    // get event group
    DD.List.IGrouping<int, int> evenGroup =
        groupedCollection.Items.Single(grouping => grouping.Key == 0);

    // check that the numbers in the even group are [2, 4, 6]
    Assert.True(evenGroup.Items.SequenceEqual([2, 4, 6]));
}
Enter fullscreen mode Exit fullscreen mode

First we populate the source collection only with odd number (1, 3 and 5):

ObservableCollection<int> sourceInts =
        new ObservableCollection<int>([1, 3, 5]);
Enter fullscreen mode Exit fullscreen mode

We use GroupWithImmutableState operator to group the source collection by the whether the number is odd or even (remainder from the division by 2):

IObservable<IChangeSet<DD.List.IGrouping<int, int>>> 
    groupedObservable =
        changeSetStream.GroupWithImmutableState(i => i % 2);
Enter fullscreen mode Exit fullscreen mode

Note that we had set using DD = DynamicData; to shorten the line.

Next, we transform the Rx observable stream of changes into IObservableList<DD.List.IGrouping<int, int>> by using AsObservableList operator:

using IObservableList<DD.List.IGrouping<int, int>> 
    groupedCollection =
        groupedObservable
            .AsObservableList();
Enter fullscreen mode Exit fullscreen mode

AsObservableList() calls Subscribe() underneath and the resulting IObserabableList has to be disposed. This is why we employ using keyword.

Next we check that the result has only one group (of odd numbers) and that it consists of number 1, 3 and 5:

Assert.True(groupedCollection.Count == 1);

// get odd group
DD.List.IGrouping<int, int> oddGroup =
    groupedCollection.Items.Single(grouping => grouping.Key == 1);

Assert.True(oddGroup.Items.SequenceEqual([1, 3, 5]));
Enter fullscreen mode Exit fullscreen mode

Now, let us add even numbers 2, 4 and 6 to the source collection:

sourceInts.AddRange([2, 4, 6]);
Enter fullscreen mode Exit fullscreen mode

The resulting groupedCollection will now consist of two groups (one containing odd and the other even numbers):

Assert.True(groupedCollection.Count == 2);
Enter fullscreen mode Exit fullscreen mode

The odd group will contain numbers 1, 3 and 5, while the even group will contain numbers 2, 4 and 6:

oddGroup = 
    groupedCollection.Items.Single(grouping => grouping.Key == 1);

// check that the numbers in the odd group are [1, 3, 5]
Assert.True(oddGroup.Items.SequenceEqual([1, 3, 5]));

// get event group
DD.List.IGrouping<int, int> evenGroup =
    groupedCollection.Items.Single(grouping => grouping.Key == 0);

// check that the numbers in the even group are [2, 4, 6]
Assert.True(evenGroup.Items.SequenceEqual([2, 4, 6]));
Enter fullscreen mode Exit fullscreen mode

Grouping with Binding and Aggregation Sample

Previous sample produces an ObservableList of groups. This sample will produce an observable collection of objects of IntsGroup type which is a wrapper around Dynamic Data's IGroup<int, int> type with added Sum property representing the sum of all integers within the group:

public class IntsGroup : IDisposable
{
    // Dynamic Data group
    public IGroup<int, int> Group { get; }

    // group key
    public int GroupKey => Group.GroupKey;

    // collection of integers 
    // within the group
    public IEnumerable<int> Values => Group.List.Items;

    // represents sum of all integers within the group
    public int Sum { get; set; }

    // handle to dispose the subscription
    private IDisposable? _aggregationDisposable = null;

    // pass DynamicData group of integers.
    public IntsGroup(IGroup<int, int> group)
    {
        Group = group;

        // every time the collection changes,
        // change the Sum property to reflect 
        // the sub of interges within the collection
        _aggregationDisposable =
            group
                .List
                .Connect()
                .ToCollection()
                .Select(collection => collection.Sum())
                .Subscribe(sum => Sum = sum);
    }

    public void Dispose()
    {
        if (_aggregationDisposable != null)
        {
            // need to dispose the subscription 
            _aggregationDisposable?.Dispose();

            _aggregationDisposable = null;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

To make sure that Sum property changes every time collection integers are added or removed from the group, we subscribe to the group's List changes within the constructor:

public IntsGroup(IGroup<int, int> group)
{
    Group = group;

    // every time the collection changes,
    // change the Sum property to reflect 
    // the sub of interges within the collection
    _aggregationDisposable =
        group
            .List
            .Connect()
            .ToCollection()
            .Select(collection => collection.Sum())
            .Subscribe(sum => Sum = sum);
}
Enter fullscreen mode Exit fullscreen mode

Here is the code for the test:

[Fact]
public static void GroupingWithBindingAndAggregationTest()
{
    // create source collection and populate
    // it with numbers 1 to 6.
    ObservableCollection<int> source =
        new ObservableCollection<int>(Enumerable.Range(1, 6));

    // create a stream of IChangeSet<int> parameters
    // from the source collection
    IObservable<IChangeSet<int>> sourceChangeObservable =
        source.ToObservableChangeSet();

    // create a collection of IntsGroup objects
    // representing the grouped integers
    IObservableCollection<IntsGroup> groupedInts = 
        new ObservableCollectionExtended<IntsGroup>();

    // using clause is to remove the 
    // subscription by the end of the method.
    using IDisposable disposableSubscription =
        sourceChangeObservable

            // group by remainder from division by 2
            // (even vs odd numbers)
            .GroupOn(i => i % 2)

            // transform each group
            // to IntsGroup object
            .Transform(g => new IntsGroup(g))

            // bind to groupedInts collection
            .Bind(groupedInts)

            // make sure once an IntsGroup
            // object is removed from the collection,
            // it is also deleted
            .DisposeMany()

            // Subscribe - start pulling
            // the changes
            .Subscribe();

    // get the even group of integers
    IntsGroup evenGroup =
        groupedInts.FirstOrDefault(g => g.GroupKey == 0)!;

    // get the odd group of integers
    IntsGroup oddGroup =
        groupedInts.FirstOrDefault(g => g.GroupKey == 1)!;

    // assert that the even group has 2, 4 and 6 values in it
    Assert.True(evenGroup.Values.SequenceEqual([2, 4, 6]));

    // assert that the Sum property of the even group
    // equals to the sum of 2, 4 and 6
    Assert.Equal(2 + 4 + 6, evenGroup.Sum);

    // assert that the odd group has 1, 3 and 5 values in it
    Assert.True(oddGroup.Values.SequenceEqual([1, 3, 5]));

    // assert that the odd group's Sum property 
    // equals to the sum of 1, 3 and 5
    Assert.Equal(1 + 3 + 5, oddGroup.Sum);

    // add odd number 7 to the source 
    source.Add(7);

    // Assert that the even group remained the same
    Assert.True(evenGroup.Values.SequenceEqual([2, 4, 6]));

    // Assert that even group's Sum remains the same
    Assert.Equal(2 + 4 + 6, evenGroup.Sum);

    // make sure that odd group added number 7 at the end
    Assert.True(oddGroup.Values.SequenceEqual([1, 3, 5, 7]));

    // Assert that the odd group's Sum property 
    // equals to the sum of 1, 3, 5 and 7
    Assert.Equal(1 + 3 + 5 + 7, oddGroup.Sum);
}
Enter fullscreen mode Exit fullscreen mode

Note that we are using Transform(...) and Bind(...) methods to populate the observable collection IObservableCollection<IntsGroup> groupedInts:

// using clause is to remove the 
// subscription by the end of the method.
using IDisposable disposableSubscription =
    sourceChangeObservable

        // group by remainder from division by 2
        // (even vs odd numbers)
        .GroupOn(i => i % 2)

        // transform each group
        // to IntsGroup object
        .Transform(g => new IntsGroup(g))

        // bind to groupedInts collection
        .Bind(groupedInts)

        // make sure once an IntsGroup
        // object is removed from the collection,
        // it is also deleted
        .DisposeMany()

        // Subscribe - start pulling
        // the changes
        .Subscribe();
Enter fullscreen mode Exit fullscreen mode

Then we assert the correct collection content and the correct sum of the items within the even and odd groups:

// get the even group of integers
IntsGroup evenGroup =
    groupedInts.FirstOrDefault(g => g.GroupKey == 0)!;

// get the odd group of integers
IntsGroup oddGroup =
    groupedInts.FirstOrDefault(g => g.GroupKey == 1)!;

// assert that the even group has 2, 4 and 6 values in it
Assert.True(evenGroup.Values.SequenceEqual([2, 4, 6]));

// assert that the Sum property of the even group
// equals to the sum of 2, 4 and 6
Assert.Equal(2 + 4 + 6, evenGroup.Sum);

// assert that the odd group has 1, 3 and 5 values in it
Assert.True(oddGroup.Values.SequenceEqual([1, 3, 5]));

// assert that the odd group's Sum property 
// equals to the sum of 1, 3 and 5
Assert.Equal(1 + 3 + 5, oddGroup.Sum);
Enter fullscreen mode Exit fullscreen mode

Then we add one more (odd) number 7 to the source collection and re-test that the even group and its sum remained the same while the odd group and its sum changed correspondingly:

// add odd number 7 to the source 
source.Add(7);

// Assert that the even group remained the same
Assert.True(evenGroup.Values.SequenceEqual([2, 4, 6]));

// Assert that even group's Sum remains the same
Assert.Equal(2 + 4 + 6, evenGroup.Sum);

// make sure that odd group added number 7 at the end
Assert.True(oddGroup.Values.SequenceEqual([1, 3, 5, 7]));

// Assert that the odd group's Sum property 
// equals to the sum of 1, 3, 5 and 7
Assert.Equal(1 + 3 + 5 + 7, oddGroup.Sum);
Enter fullscreen mode Exit fullscreen mode

Image of Datadog

The Essential Toolkit for Front-end Developers

Take a user-centric approach to front-end monitoring that evolves alongside increasingly complex frameworks and single-page applications.

Get The Kit

Top comments (0)

Sentry image

See why 4M developers consider Sentry, “not bad.”

Fixing code doesn’t have to be the worst part of your day. Learn how Sentry can help.

Learn more

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay