3 Comments

Explorations in Adapting Redux to C#

We’ve been using Redux a lot lately at Atomic, and we’ve come to appreciate how a centralized holder of application state can simplify application architecture. In a few different projects, we’ve used this architecture as inspiration for mobile applications in Objective C, Java (for Android), and Swift.

Independently, we’ve also been using Xamarin on our mobile applications to ease development and maintenance of apps running across both major platforms, whether via native UI frameworks, Xamarin.Forms, or hybrid web apps, in which the UI layer is rendered in a web view and business, persistence, and server synchronization logic is implemented in C#.

On my current project, I’ve been working toward evolving a similar model to drive state management in our Xamarin.Forms application, with the aim of creating a reusable set of tools for any MVVM-based C# application. Even though Redux.NET looks great, I was interested in taking a fresh approach to explore a few alternative design decisions. While we may move toward using Redux.NET in the future, our approach has a few interesting properties that are working out well.

The Redux Model

In a typical MVVM application, view models each have their own internal mutable state which is changed in response to user interactions and application events. These state changes fire property change events that allow the system to react to user interactions, or for the UI to update to reflect changes that occur for other reasons, such as persistence, Bluetooth, or network events.

Redux moves this state out of the view models and into one central application store. Instead of having a set of mutable variables, that application store has one single reference to an immutable data structure that represents a complete snapshot of the application state.

This state is never mutated directly. Instead, POCO (plain old C# object) action objects are dispatched to the store. In response to this action, a new application state is created to replace the old one, and the rest of the application reacts to the change. See the Redux.NET or Redux documentation for examples.

Actions and Projections

One area where our approach to the model has diverged from Redux.NET’s direct reimplementation of Redux via Rx takes inspiration from work by others at Atomic–Chris Farber, Job Vranish, and Patrick Bacon.

In a typical Redux application, code that needs access to data in the Store maps the application state data structure into the subset of data needed by a particular component or view model. For example, this bit of code from a Redux.NET example returns the set of todo items whose status matches a chosen filter in a todo application:

   public sealed partial class MainSection : UserControl
    {
        public MainSection()
        {
            this.InitializeComponent();

            App.Store
                .Subscribe(state => TodosItemsControl.ItemsSource = GetFilteredTodos(state));
        }

        public static IEnumerable<Todo> GetFilteredTodos(ApplicationState state)
        {
            if (state.Filter == TodosFilter.Completed)
            {
                return state.Todos.Where(x => x.IsCompleted);
            }

            if (state.Filter == TodosFilter.InProgress)
            {
                return state.Todos.Where(x => !x.IsCompleted);
            }

            return state.Todos;
        }
    }

While this approach works, it unfortunately complects the “what” needed by the component with the “how.” “The current set of todos matching the filter” is a domain-level concept, and “how” it is computed occurs directly within the component.

Our goal is to establish an API and programming patterns that encourage encoding the “how” of these domain concepts in the domain layer, leaving view models and components to merely describe which domain needs and events are are mapped to the component’s properties.

To solve this problem, our Store implementation does not expose a public IObservable of the state. Instead, information is always extracted from the Store via a “projection”–a function from our State type to some other type which can be used to extract information. Crucially, our projections are defined in a namespace which is logically part of our shared domain model, and not directly within our view models.

In our model, this subscription would instead look like:

 App.Store.Observe(Projections.Todos.FilteredTodos)
	.Subscribe(visibleTodos => TodosItemsControl.ItemsSource = visibleTodos);

where Projections.Todos.FilteredTodos is a static function, defined in exactly the same way, located in our catalog of named projections elsewhere in the program.

This change in organization hoists our domain model, as represented by interactions with our store, up a level of abstraction. Instead of dispatching actions and seeing changes in our application state data structure, our domain model is modeled by invariant relationships between actions and projections.

Our projections represent a named library of “whats” that can be asked of our application state, and the “how” of their computation is irrelevant to other parts of our application. We are free to change the shape of our application’s state, provided the meaning of our actions and projections is preserved.

This allows us to test the invariants of our domain protocol directly in unit tests. For example, this unit test snippet from our application shows how certain projections change in response to actions dispatched by our Bluetooth Low Energy subsystem:

// initialize
store.Dispatch(new BluetoothAvailableEvent());
store.Project(Projections.Ble.ConnectionState).Should().Be(ConnectionState.Disconnected);

// scanning
store.Dispatch(new StartScanningEvent());
store.Project(Projections.Ble.ConnectionState).Should().Be(ConnectionState.Scanning);
store.Dispatch(new FinishedScanningEvent());
store.Project(Projections.Ble.ConnectionState).Should().Be(ConnectionState.Disconnected);

Project is just like Observe, but returns an instantaneous snapshot of the state instead of an IObservable. It takes the same projections as arguments, but returns the current value, not a signal that evolves over time.

This test proves that in response to our various BLE actions, our projections respond in the desired way. The relationship between actions and projections represent core meaning about how our application works logically, independent of specific components or subsystems. Any part of the application is free to use these projections to ask the same question, and it’s guaranteed to be consistent with any other part of the system that needs the same thing.

Threading and Asynchrony

One other change we’ve made to help the Redux model fit nicely into the multithreaded world of C# is our approach to multithreading.

Redux.NET dispatches actions on a subject, which publishes the change on the current thread. This can pose a problem in applications, where UI components must always be updated on the same thread.

Our Store decouples the effect of an action from its publication. We allow dispatching events from any thread. The update to the state owned by our Store happens synchronously, such that, locally to any thread, the relationship between actions and projections is consistent. However, signals created via Observe are not guaranteed to have been notified.

Instead, we always publish the new results of projections on the main UI thread. It is always safe to bind a signal from Observe to part of the UI, optimizing for the common case and ensuring that any handler running in response to a signal is running in a sensible place. Observers which wish to do something in a different synchronization context in response to an event must explicitly switch over to the desired context.

Testing in an Asynchronous World

Without care, our guarantee of running observers on the main thread could complicate testing. If a test wishes to dispatch an action and see that a view model property changed in response, as defined above, there is no way to wait until the view model has received the published value to see that it changed appropriately.

To support this use case, we provide an additional dispatch helper: DispatchAndPublishAsync. This async dispatch function returns a Task, which completes once publication of the dispatched event has occurred. This allows us to test our view models:

await store.DispatchAndPublishAsync(new LogInFinishedEvent { FirstName = "Drew" });
vm.Name.Value.Should().BeEquivalentTo("Drew");

Alternatively, we can await the publication of any actions in flight at a point in time. This is equivalent to the above:

store.Dispatch(new LogInFinishedEvent { FirstName = "Drew" });
await store.AllPublished;
vm.Name.Value.Should.BeEquivalentTo("Drew");

This gives us a lot of control. We can test larger subsystems of our application and ensure our tests can proceed in lockstep with the actions occurring under the hood.

If a Tree Mutates in the Forest…

The biggest divergence of our model from classic Redux actually stems from immutability. C# makes creating immutable data structures cumbersome. To simplify programming, our Store uses not one single mutable reference to an immutable structure, but a family of references to immutable structures that are always updated transactionally in unison.

Specifically, we’re representing our State as a set of nested structs which contain references to immutable data structures and reference types. We’re using structs to organize our State into logical sections, and using pure immutable values for the constituents.

For example, our State has a BleState, which is defined as:

    public struct BleState
    {
        // peristed between sessions
        public ImmutableHashSet<PeripheralId> KnownPeripherals;
        public ImmutableList<SensorMetadata> KnownSensors;

        // pairing process data
        public ImmutableDictionary<Peripheral, SignalStrength> DiscoveredPeripherals;
        public Peripheral? ConnectingPeripheral;

        // transient data
        public ImmutableDictionary<SensorType, ImmutableList<SensorDataValue>> ConnectedSensors;
        public Peripheral? ConnectedPeripheral;
        public ConnectionState ConnectionState;

        public Peripheral? ActivePeripheral => ConnectingPeripheral ?? ConnectedPeripheral;
    }

Our action handlers respond not by creating a new struct, but by mutating a reference to the authoritative State instance. Additionally, projections access the mutable struct directly.

Heresy! I hear you thinking. Mutable structs are evil! Yes. They are.

But. The whole point of this architecture is that it’s the actions and projections which are used by the outside world, and it’s only those elements that ever access the mutable data, and only in a carefully controlled, synchronized context.

C#, unfortunately, still does not have convenient ways of creating immutable classes with a convenient copy-and-update mechanism, deep equality semantics, and a sensible GetHashCode implementation. (And this was cut from C# 7. 😢) By representing our State with structs, our action handlers can be written conveniently as:

private static void HandleBleConnected(ref BleState bleState, BleConnectedEvent evt)
{
    bleState.KnownPeripherals = bleState.KnownPeripherals.Add(evt.Peripheral.Id);
    bleState.ConnectionState = ConnectionState.Connected;
    bleState.KnownSensors = bleState.KnownSensors.Concat(evt.Sensors).GroupBy(c => c.SensorType).Select(g => g.First()).ToImmutableList();
    bleState.ConnectingPeripheral = null;
    bleState.ConnectedPeripheral = evt.Peripheral;
    bleState.ConnectedSensors = evt.Sensors.ToImmutableDictionary(c => c.SensorType, c => ImmutableList<SensorDataValue>.Empty);
    bleState.DiscoveredPeripherals = bleState.DiscoveredPeripherals.Clear();
}

without the need for a lot of boilerplate update functions which mostly just add development overhead.

Furthermore, our Store implementation checks that no projection mutates the State in debug builds, proving that we’re being true to the Redux architecture. These structs are meant to organize our State, and none of them are meant to escape from a projection. But even if they did, value-type semantics ensure that no external viewer can ever mutate the real State. Value semantics also allow us to easily snapshot our State by simply copying the struct.

We took this approach out of convenience, and switching to an entirely immutable representation may still be in the cards. But so far, this approach has been convenient and hasn’t led to any problems, nor has it compromised the benefits of the overall architecture. If we change our minds, we can update our types, projections, and event handlers, and the rest of our application will never know the difference.

Future Work

We’re not quite ready to release anything yet. We need to make a final decision about structs versus immutable classes. More importantly, we want to have a good pattern established for side effects. In particular, we’re looking to Redux Sagas for inspiration.