An Immutable Asynchronous State Holder in C#

Managing state in a clean way is probably one of the most challenging aspects of many software projects. When using asynchronous programming technologies such as .NET’s async/await functionality or the Reactive Observable pattern, the problem of state management is exacerbated and often becomes a source of errors.

In several projects that I have worked on over the last few years, we instituted a StateHolder class that eases the burden of managing state. It does so by:

  • Supporting atomic updates to one or more items in the state (thread-safe)
  • Supporting state change subscriptions so that observers can immediately react to state changes
  • Providing an asynchronous interface for publishing updates to the state (non-blocking)

Recently, I implemented a StateHolder that has these features in C#, and I thought I’d share my results.

How Do You Use It?

Before I get into the details of the implemention, I’ll demonstrate how it’s used. We’ll start by assuming that a struct called AppState exists, and it defines a structure of the held state. The elements in the state are exposed as get-only properties like this:


public struct AppState
{
  public int TargetTemperature { get; private set; }

  public AppState(int targetTemperature) {
    TargetTemperature = targetTemperature;
  }
}

Additionally, we’ll assume that a class called AppStateHolder exists, deriving from StateHolder and specifying that the held state is our AppState struct.

Finally, the AppStateHolder can be used like this:


// Create an instance of AppStateHolder (or get it from DI) and pass it around throughout the application. It's safe to use anywhere!
AppStateHolder appStateHolder = new AppStateHolder();

// Setup an observer on the TargetTemperature field
appStateHolder.TargetTemperatureChanges.Subscribe(newTargetTemp => {
  Console.WriteLine($"The target temperature is now: {newTargetTemp}");
});

// Update the TargetTemperature (But continue on before the state change is actually executed)
await appStateHolder.UpdateState(stateBuilder => {
  // Here stateBuilder is setup with the the current values in the held AppState
  stateBuilder.TargetTemperature += 5;
  return stateBuilder;
});

// Produces Output:
//   The target temperature is now: 0
//   The target temperature is now: 5

// Or, if you want to wait for the state change to be executed before continuing on Unwrap the result of updateState like this:
await appStateHolder.UpdateState(stateBuilder => {
  stateBuilder.TargetTemperature += 5;
  return stateBuilder;
}).Unwrap();

From a usage standpoint, that’s all there is to it. The AppStateHolder defines Observables for each property which can be subscribed to in order to receive updates when a component of the held state changes.

The AppStateHolder instance also exposes a method called UpdateState, which is used to enqueue atomic updates to the state. The stateBuilder parameter that is passed to the update lambda is a mutable version of the AppState (an AppStateBuilder) that is initialized from the current value of the held state.

The return value of UpdateState is a Task<Task>. Awaiting the outer task will wait until the update transaction has been queued up, but execution will continue before the transaction is actually executed. You can await the execution of the update transaction by unwrapping the result task and thus waiting on the inner task.

How Do You Make the AppStateHolder?

AppStateHolder is just a name of a class that holds a group of application state. Some people like to put all of their state in a single class, while others like to break it up into multiple classes. Both are possible and easy to do with this implementation of the StateHolder. For example, you could make separate instances for an AccountStateHolder and a UserStateHolder without having to duplicate code.

In my example, the AppStateHolder is just a class that derives from StateHolder and specifies the type for the held state and the held state builder. It also uses the Project() function to create any observables that are needed for subscribing to changes to individual properties of the state.

public class AppStateHolder : StateHolder<AppState, AppStateBuilder>
{
  public IObservable TargetTemperatureChanges => Project(state => state.TargetTemperature);
}

How Do You Define the Held State?

Defining the held state is done by defining two parts: the state and a builder. The builder has the same properties as the held state, but its properties are mutable and it contains functions for converting between the AppState and the AppStateBuilder.

public struct AppState
{
  public int TargetTemperature { get; private set; }
  public AppState(int targetTemperature) {
    TargetTemperature = targetTemperature;
  }
}

public struct AppStateBuilder : IHeldStateBuilder<AppState, AppStateBuilder>
{
  public int TargetTemperature;

  public void InitializeFrom(AppState appState)
  {
    TargetTemperature = appState.TargetTemperature;
  }

  public AppState Build()
  {
    return new AppState(TargetTemperature);
  }

  public AppState DefaultState()
  {
    return new AppState(
      0 // Default Value for TargetTemperature
        // additional defaults values go here...
    );
  }
}

The state builder struct implements the IHeldStateBuilder interface, which defines three methods that are essential for making updates to the state.

  • InitializeFrom(): Initializes the builder with the current values from the AppState
  • Build(): Constructs a new AppState from the builder
  • DefaultState(): Constructs a new AppState with default values

Whenever a new property is added to the AppState, you must update the AppStateBuilder methods, as well.

How Does the StateHolder Work?

The full implementation of the StateHolder class is shown below. The key component is the transactionBuffer, which is implemented with an ActionBlock. If you’ve never heard of an ActionBlock, I would highly recommend reading up on Microsoft’s TPL Dataflow library (Task Parallel Library) and the components within. Dataflow provides building blocks which can be chained together to create complex asynchronous operations. In this case, I am only using one component of it, the ActionBlock.

An ActionBlock is a type of buffer to which items can be added via an async interface. When data is received, it is processed by the lambda that is passed to its constructor. It is possible to configure an ActionBlock so that it can process multiple items from the buffer concurrently.

When you call stateHolder.UpdateState(…), the update lambda that you pass in ends up being sent into the transactionBuffer ActionBlock. The ActionBlock is configured to allow only one item to be processed at a time, ensuring that the state changes will be processed atomically, no matter what thread called them.

Once the state change transaction is complete, the new state is sent on a BehaviorSubject, which allows subscribers to be notified of the change and receive a copy of the updated state. A BehaviorSubject is used so that whenever an observer subscribes, it will immediately receive the current app state, guaranteed.

public abstract class StateHolder<THeldState, THeldStateBuilder> where THeldState : struct where THeldStateBuilder : struct, IHeldStateBuilder<THeldState, THeldStateBuilder>
{
  delegate void TransactionFunc(BehaviorSubject heldStateSubject);
  public IObservable CurrentState { get; private set; }
  BehaviorSubject currentStateSubject = new BehaviorSubject(new THeldStateBuilder().DefaultState());
  public THeldState LatestState => currentStateSubject.Value;
  ActionBlock transactionBuffer;

  public StateHolder()
  {
    transactionBuffer = new ActionBlock(transaction => {
      // This block will run serially because MaxDegreeOfParallelism is 1
      // That means we can read from and modify the current state (held in the subject) atomically
      transaction(currentStateSubject);
    }, new ExecutionDataflowBlockOptions { MaxDegreeOfParallelism = 1 });

    CurrentState = currentStateSubject.DistinctUntilChanged();
  }

  public async Task UpdateState(Func<THeldStateBuilder, THeldStateBuilder> updateBlock)
  {
    var taskCompletionSource = new TaskCompletionSource();
    TransactionFunc updateTransaction = currentStateSubject => {
      var builder = new THeldStateBuilder();
      builder.InitializeFrom(currentStateSubject.Value);
      try {
        var newState = updateBlock(builder).Build();
        currentStateSubject.OnNext(newState);
      } catch (Exception ex) {
        Console.WriteLine("ERROR: Update state transaction failed");
        Console.WriteLine(ex);
      }
      taskCompletionSource.SetResult(Unit.Default);
    };
    var didSend = await transactionBuffer.SendAsync(updateTransaction);

    if (!didSend) {
      throw new ApplicationException("UpdateState failed to process transaction. This probably means the BufferBlock is not initialized properly");
    }
    return taskCompletionSource.Task;
  }

  // Use this in xStateHolder class to make observable projections of elements in the state.
  public IObservable Project(Func<THeldState, T> block)
  {
    return CurrentState.Select(block).DistinctUntilChanged();
  }
}

public interface IHeldStateBuilder<TState, TBuilder>
{
  void InitializeFrom(TState state);
  TState Build();
  TState DefaultState();
}

Usage Advice

Once you start using a global state holder like this, your next question will likely be, “What state should/should not go into it?” I follow a couple of rules to help answer that question:

First, don’t put anything in the app state that is merely a derived field of other items in the state. That is essentially double-encoding your state and will often lead to problems.

Second, don’t put something in the state that is only going to be consumed by code within the same class. One of the biggest advantages of using this state holder mechanism is that it allows for a cleaner separation of concerns within your app. Often, the producer of a piece of state is completely unrelated, and it need not have any knowledge of the consumer of that state. This is a perfect use case for a state holder.

For example, imagine you have a class in a mobile app that is set up to receive callbacks from the OS when the background/foreground state of the application changes. This class could derive a value that represents the current foreground status and update it in the state holder whenever it changes.

Now, this class does not need to, and should not, have any knowledge of other parts of the application that need to consume that status. That is not its concern. Furthermore, the other parts of the application that need to consume the foreground status should not be concerned with where that status came from or how it was produced. Their only concern is whatever needs to be done when that status changes. The state holder described above makes this separation possible.

I’d love to hear your approaches to managing state in a clean way.

Conversation
  • Looks like a very helpful way to manage state. Great walk through of the usage.

    Any thoughts about how to reduce the boilerplate in the builder and holder that reference the AppState.TargetTemperature? Been struggling with this same sort of thing in a Java application (which is already much worse than in C# since we don’t have structs and properties!).

    • Thanks, Galen. I agree it would be nice to reduce the boilerplate. One thought I had is that you could add a public setter to all the properties in the AppState struct, thus making them mutable. Then you wouldn’t need the AppStateBuilder class at all because you could just pass an instance of AppState to the update block. The downside of this is that it could create possible confusion if someone wasn’t thinking and tried to mutate a property on a value then was sent to a state change subscriber. Because all of the HeldState values are structs, and thus value types, if a subscriber did modify the value that was sent to it, it won’t hurt anything because they are just modifying a copy of the state, but it likely won’t have the affect they were hoping for!

      If you have any other ideas I’d love to hear them.

      • I’m hoping to explore an approach that uses “type safe heterogeneous containers”, i.e. maps where the key is parameterized with the type of the value. So rather than using a class or struct for the AppState, it is a map with “symbol” objects for keys.

        One downside is instead of the usual access/assign syntax for properties, you have to do it through methods that take the symbol as an argument. For example (in Java):

        Symbol targetTemperature = symbol(“targetTemperature”); // public static final, defined in a class with other relevant symbols for this state

        SymbolMap state = …;
        state.put(targetTemperature, 70);
        int t = state.get(targetTemperature);

        Described here:
        https://stevewedig.com/2014/08/28/type-safe-heterogenous-containers-in-java/

        I think the benefit of being able to write general code to work with the data in the map will greatly outweigh the downsides of the different-looking access/assign syntax and having to use symbol objects instead of the usual properties/fields. For example, you can use the fluid/solid methods on the SymbolMap to get mutable/immutable copies without having to create a builder. Another downside is you are working with plain data instead of objects, but that’s a totally reasonable and necessary trade off in many situations (a class with some additional domain logic could be a wrapper for a SymbolMap if desired).

        This is basically data handling ala Clojure, more verbose here of course, but you do get the benefit of type safety.

  • Comments are closed.