This is a day job project that for a bit I thought might spin out into an open source library but the funny thing about it is that it has been more of a chisel the statue out of the marble type of project. It started from a complicated place and the further we’ve pushed it towards Redux best practices the more it disappears into “just” being primarily a pattern of C#’s out-of-the-box dependency injection.

(For some small context, the original design that this design migrated away from was far more inspired as something of a telephone game to Angular’s libraries such as NgRx, which themselves were a telephone game away from Redux.)

It still seems worth publicly documenting for the next team looking for this pattern. I wish there was a library I could point to to install this pattern into your project, hopefully this documentation will suffice as a good starting point. To some extent, the lack of a need for an external library is a small triumph and a proof that this can be a good way to build your C# “view state” model, for projects that want a ReactiveX-“native” alternative to state management and/or event bus patterns such as MVC, MVVM, Mediatr.

RootAction

The core type for this Redux design is RootAction. This is not a particularly great name, but it does avoid conflict with System.Action<T> (the void-relative of System.Func<T, U>). In proper Redux fashion, this is a very simple type with only a required field name Type.

public record RootAction
{
    public virtual string Type => $"{GetType().Name}";
}

There are a bunch of strong notes to be made here, some of which remained debates for some time after moving to this pattern. The NgRx family never fully understood the importance of Actions as a serializable type, but it is a useful core principle to many Redux pattern benefits. Serializable types can be output to logs for replay, for instance.

Related to replay, that is something the day job project never got around to implementing. At one point Source Generators were explored to replace the reflection-based Type seen here with something more useful to System.Text.Json’s polymorphic support.

The advantage to this form of base Type implementation was that a tree of types was relatively easy to build. Example:

public record CoolFeatureAction : RootAction
{
    public override string Type => $"CoolFeature/{GetType().Name}"
}

public sealed record CoolFeatureInitialized(bool IsCool) : CoolFeatureAction;

One of the things that this also highlights is that the Redux world best practice of writing “action creators” is handled automatically by C# record syntax. The primary record constructor is already laid out as a Redux-like “action creator”, and provides similar refactoring benefits.

A few of the other tips about Actions from the Redux pattern best practices that may be useful to keep in mind at this point: Actions are generally named like Events, they are named as the “user interaction that just happened” in the past tense. Actions should generally not be “mere setters” (FieldASet, FieldBSet) but rather State transactions as a high-level event (EntireFeatureInitialized with everything needed to update FieldA and FieldB all at once).

Option: Popping the Middle Layer of Actions

Another brief digression from JSON polymorphic debates was if the RootAction’s Type property implementation should have been, assuming Reflection was still fine:

public record RootAction
{
    public string Type => $"{GetType().FullName.Replace('.', '/')}";
}

In which case the inheritance tree might have been a little flatter in exchange for using the namespace hierarchy automatically. (With the idea of the Replace being to still prefer the Redux dev tools suggested / namespace separator instead of .NET’s ..)

Performance Digression on Reference Types

In part from references libraries that heavily relied on .NET’s value types (typically, but not just, readonly record struct) to deeply avoid nulls (at the risk of sometimes equally confusing “zeroed” default(T) from structs) the application had picked up the premature optimization of using value types for a lot of internal state data.

This was a performance problem that we caught early in the Redux pattern refactor. It is easy to forget that value types are generally passed by value which means by copy in many cases. Especially in a Redux design you often can’t afford all the memory of every State and every Action to be copied for every Reducer (and Epic and view component, etc).

Don’t forget that public record is shorter than public readonly record struct for a good reason and is a good default. Reference types aren’t an enemy to performance in .NET, they are just as often the solution. Don’t forget to apply performance profiling tools for real data from your applications and not just default to value types because they sometimes help performance or null safety.

Reducers

The heart of the Redux pattern is Reducers to apply Actions to State. In C# we used a very simple IReducer definition from which a number of things inherited, include a Reducer<Action, State> base type for single Action reducers.

public interface IReducer<State>
{
    public State Handle(RootAction action, State state);
}

public abstract class Reducer<Action, State>
    : IReducer<State> where Action : RootAction
{
    public abstract State Reduce(Action action, State state);

    public State Handle(RootAction rootAction, State state) => rootAction switch
    {
        Action action => Reduce(action, state),
        _ => state,
    };
}

The base class is a pattern in C# you also see in things like Authorization Policy Handlers, a type-safe “trampoline” from the generic RootAction to a specific Action (we had less of a problem with a type parameter sometimes shadowing System.Action<T> than a top-level type doing so). (This is also Typescript influence here of dropping the T-prefix semi-Hungarian notation for type parameters; though as with most C# naming schemes the I-prefix for interfaces is maybe eternal at this point.)

Because the trampoline is often the shortest way to write many Reducers, the IReducer<State> method borrows the name Handle from places like Policy Handlers so that Reducer<Action, State> can use the more domain friendly Reduce as its more specific abstract function.

It’s also important to note that IReducer<State> is intentionally and only synchronous. That’s an ancient Redux best practice. That will also be something we return to as we get to Epics.

To round out our CoolFeature example we might have an example Reducer like so:

public record CoolThing(string Name, /* … */ DateTimeOffset Created);

public record CoolFeatureState(
    bool IsCool,
    DateTimeOffset CoolStarted,
    IEnumerable<CoolThing> CoolThings)
{
    public static readonly CoolFeatureState Disabled = new(false, DateTimeOffset.MinValue, []);
}

public class CoolFeatureInitializedReducer(TimeProvider timeProvider)
    : Reducer<CoolFeatureInitialized, CoolFeatureState>
{
    public override CoolFeatureState Reduce(
        CoolFeatureInitialized action,
        CoolFeatureState state) => state with
    {
        IsCool = action.IsCool,
        CoolStarted = timeProvider.GetLocalNow(),
        // ASIDE: Injecting a TimeProvider is a great way to increase
        // unit testing powers of this reducer because you can test in
        // fake time with a FakeTimeProvider.
    };
}

public static class CoolFeatureExtensions
{
    public static IServiceCollection AddCoolFeature(this IServiceCollection services) => services
        .AddTransient<IReducer<CoolFeatureState>, CoolFeatureInitializedReducer>();
}

Note that while Reducers are required to be synchronous, they are still free to inject and use other dependencies (so long as they are also synchronous). Similarly, reducers may be state machines that use complex calculations based on the current state to produce the next state. The big restriction is synchronous.

Important Digression into Lens<A, B>

Before we can talk about the Store and Slices, we need one more tool in our toolbox.

In Typescript, ala the original Redux pattern, we can easily “slice” a State via a string key of that state. Typescript is also friendly in that this can be relatively type safe thanks to meta-typing tools like T[keyof T].

Doing that type-safely in C# is a bit more work. One of the tools we can use to do it that is useful as a generic type for immutable data structures can be borrowed from functional programming and is called a Lens. A Lens is really just a type-safe getter from one item to another (given an A, I want a B), and a type-safe setter in the other direction (given a B, I would like an updated A).

There are several functional programming libraries that provide a Lens<A, B> useful in C#, including LanguageExt, but Lens<A, B> really is just as simple as described, so we can also just implement our own quite simply:

public record Lens<A, B>(
    Func<A, B> Get,
    Func<B, Func<A, A>> SetF
)
{
    public A Set(B value, A container) => SetF(value)(container);
}

There’s a few other convenience functions you could implement (often named Update), but that’s the important heart of Lens<A, B>.

The importance of SetF in this form as Func<B, Func<A, A>> as the underlying setter is maybe not obvious if you haven’t spent enough time in languages that support currying such as F# and Haskell, but is one of the useful properties for making Lens<A, B> generically useful to several of our needs.

One useful way to see it from Redux points of view is that SetF(someFixedValue) creates just about the most simple type of Reducer possible (given a State, yield the next State). It’s a handy abstraction.

Handy Digression into ShareReplay

We are taking a ReactiveX native approach to our Store that follows to encourage consistent use of Epics (more on them later). I bounce enough between RxJS and classic C# ReactiveX that I find these utility extension methods handy to keep around to save my poor memory some small bit of trouble:

public static class ObservableExtensions
{
    public static IObservable<T> Share<T>(this IObservable<T> source) =>
        source.Publish().RefCount();

    public static IObservable<T> ShareReplay<T>(
        this IObservable<T> source,
        int bufferCount) =>
        source.Replay(bufferCount).RefCount();
}

The RxJS names are little more evocative to me than remembering when to use RefCount().

ISlice<State>

Redux best practices are to use a single Store to collect your entire application state. This allows for easier debugging serialization and centralization of some forms of developer tooling.

But you often don’t want to reason with an entire application state all at once. You are often working on a specific vertical feature and want to write your Reducers and Epics (more on those later) from the perspective of only a slice of your Store.

We start with the general interface design:

public interface ISlice<State>
{
    public IObservable<State> States { get; }
}

public interface ISubSlice<ParentState> : IReducer<ParentState>
{
    public void RegisterParentSlice(ISlice<ParentState> parent);
}

ISubSlice<ParentState> is the stranger of the two interfaces in this design because it is designed to facilitate the dependency injection of circular references. Each Slice can be seen as a large reducer in its own parent. Each Slice also needs a reference to its parent to be able to do that slicing work of outputting the subset of states relevant to the slice.

The implementation of Slice follows from this dependency injection trick plus one other simple one: the .NET DI container like many DI containers supports injecting IEnumerable<T> to get all implementations of an interface that have been injected.

public class Slice<ParentState, State>
    : ISlice<State>, ISubSlice<ParentState>, IDisposable
{
    private readonly IEnumerable<IReducer<State>> reducers;
    private readonly Lens<ParentState, State> lens;
    private readonly BehaviorSubject<ISlice<ParentState>?> parentSlice = new(null);

    public IObservable<State> States => parentSlice
        .Select(parent => parent is null
            ? Observable.Empty<State>()
            : parent.States
                .Select(lens.Get)
                .DistinctUntilChanged())
        .Switch()
        .ShareReplay(1);

    public void RegisterParentSlice(ISlice<ParentState> parent) =>
        parentSlice.Next(parent);

    public Slice(
        IEnumerable<IReducer<State>> reducers,
        Lens<ParentState, State> lens
    )
    {
        this.reducers = reducers;
        this.lens = lens;
        foreach (var reducer in this.reducers)
        {
            if (reducer is ISubSlice<State> subslice)
            {
                subslice.RegisterParentSlice(this);
            }
        }
    }

    public ParentState Handle(RootAction action, ParentState parent) =>
        lens.Set(
            reducers.Aggregate(lens.Get(parent), (acc, reducer) =>
                reducer.Handle(action, acc)),
            parent);
        // or with an Update helper implemented:
        // lens.Update(parent, parentState =>
        //   reducers.Aggregate(parentState, (acc, reducer) =>
        //     reducer.Handle(action, acc)))

    public void Dispose()
    {
        GC.SuppressFinalize(this);
        parentSlice.Dispose();
    }
}

public static class SliceExtensions
{
    public static IServiceCollection AddSlice<ParentState, State>(
        Lens<ParentState, State> lens) => services
            .AddSingleton(provider => new Slice(
                provider.GetServices<IReducer<State>>(),
                lens))
            // allow direct injection IObservable<State>
            .AddSingleton(provider => provider
                .GetRequiredService<Slice<ParentState, State>>()
                .States)
            .AddSingleton<ISlice<State>>(provider => provider
                .GetRequiredService<Slice<ParentState, State>>())
            // register for pickup by its parent
            .AddSingleton<IReducer<ParentState>>(provider => provider
                .GetRequiredService<Slice<ParentState, State>>());
}

Our CoolFeatureState might be part of a larger RootState:

public record RootState(CoolFeatureState CoolFeature)
{
    public static readonly RootState Initial = new(CoolFeatureState.Disabled);

    public static readonly Lens<RootState, CoolFeatureState> CoolFeatureLens =>
        new(state => state.CoolFeature, state => coolFeature => state with
        {
            CoolFeature = coolFeature,
        });
}

With Slice implemented we can fill out our example CoolFeatureExtensions with the next registration piece:

public static class CoolFeatureExtensions
{
    public static IServiceCollection AddCoolFeature(this IServiceCollection services) => services
        .AddTransient<IReducer<CoolFeatureState>, CoolFeatureInitializedReducer>()
        .AddSlice(RootState.CoolFeatureLens);
}

Note that both generic parameters to AddSlice are cleanly picked up from Lens<A, B> matching those parameters.

Store

I think the implementation of our central Redux Store follows pretty directly from our Slice implementation above:

public IActions
{
    public IObservable<RootAction> Actions;
}

public IDispatch
{
    public void Dispatch(RootAction action);
}

public sealed class Store
    : ISlice<RootState>, IActions, IDispatch, IDisposable
{
    private readonly IEnumerable<IReducer<RootState>> reducers;
    private readonly Subject<RootAction> actions = new();
    private readonly BehaviorSubject<RootState> states = new(RootState.Initial);

    public IObservable<RootState> States => states;
    public IObservable<RootAction> Actions => actions;

    public Store(IEnumerable<IReducer<RootState>> reducers)
    {
        this.reducers = reducers;
        foreach (var reducer in reducers)
        {
            if (reducer is ISubSlice<RootState> subslice)
            {
                subslice.RegisterParentSlice(this);
            }
        }
    }

    public void Dispatch(RootAction action)
    {
        var nextState = reducers.Aggregate(states.Value, (acc, reducer) =>
            reducer.Handle(action, acc))
        if (nextState != states.Value)
        {
            states.OnNext(nextState);
        }
        actions.OnNext(action);
    }

    public void IDispose()
    {
        GC.SuppressFinalize(this);
        states.Dispose();
        actions.Dispose();
    }
}

public static class StoreExtensions
{
    public static IServiceCollection AddStore(this IServiceCollection services) => services
        .AddCoolFeature()
        .AddSingleton<Store>()
        .AddSingleton(provider => provider.GetRequiredService<Store>().States)
        .AddSingleton<ISlice<RootState>>(provider => provider.GetRequiredService<Store>())
        .AddSingleton(provider => provider.GetRequiredService<Store>().Actions)
        .AddSingleton<IActions>(provider => provider.GetRequiredService<Store>())
        .AddSingleton<IDispatch>(provider => provider.GetRequiredService<Store>());
}

IEpic<State>

The final part to this pattern that does a surprising amount of heavy lifting is the concept of an Epic. Named after “lengthy story”, an Epic is the asynchronous workflow relative of the Reducer. Where Reducers must be synchronous, the Epic can do anything Observables can do, including calling async/await methods and running all sorts of side effects.

Generally Epics observe Actions, the current state after an Action, and return Actions.

public interface IEpic<State>
{
    public string Name { get; }

    public IObservable<RootAction> Create(
        IObservable<RootAction> actions,
        IObservable<State> states,
        IScheduler scheduler);
}

The IScheduler is explicitly provided to encourage testing with TestScheduler and also because we found reasons at runtime to inject specific schedulers without the Epic needing to be aware which.

The Name is useful for logging and debugging.

An example “Cool Feature” Epic might look something like:

public record CoolFeatureLoaded(IEnumerable<CoolThing> CoolThings)
    : CoolFeatureAction;

public class CoolFeatureLoadedReducer : Reducer<CoolFeatureLoaded, CoolFeatureState>
{
    public CoolFeatureState Reduce(
        CoolFeatureLoaded action,
        CoolFeatureState state
    ) =>
        state with { CoolThings = action.CoolThings };
}

/// <summary>
/// When Cool Feature has initialized, load any applicable CoolThings.
/// </summary>
public class CoolThingLoader(ICoolThingService service)
    : IEpic<CoolFeatureState>
{
    public string Name => nameof(CoolThingLoader);

    public IObservable<RootAction> Create(
        IObservable<RootAction> actions,
        IObservable<CoolFeatureState> states,
        IScheduler scheduler
    ) =>
        actions.OfType<CoolFeatureInitialized>()
            // Sometimes a .WithLatestFrom() is useful here if the
            // reducer calculates something, say in this case the
            // API wants the calculated CoolStarted time. Actions are
            // the "transaction model" for the Store, so the state
            // after an Action is observed should always reflect all of
            // the Reducers have run.
            .Select(action => action.IsCool
                ? Observable.FromAsync(async (ct) =>
                    new CoolFeatureLoaded(
                        await service.LoadCoolThings(ct)),
                    scheduler)
                : Observable.Return(new CoolFeatureLoaded([])))
            .Switch();
}

// updated registration
public static class CoolFeatureExtensions
{
    public static IServiceCollection AddCoolFeature(this IServiceCollection services) => services
        .AddTransient<IReducer<CoolFeatureState>, CoolFeatureInitializedReducer>()
        .AddTransient<IReducer<CoolFeatureState>, CoolFeatureLoadedReducer>()
        .AddTransient<IEpic<CoolFeatureState>, CoolThingLoader>()
        .AddSlice(RootState.CoolFeatureLens);
}

Responsibility Digression about Epics

One thing that should be obvious from the Epic interface is that an Epic may return the same Action it observes in the first place. This is a recipe for an infinite loop.

The key wisdom when writing an Epic is that often attributed to Spider-Man’s Uncle Ben:

With great power comes great responsibility.

Loops are a key flow control power in software development. Just as you might use a while loop, you may find needs for Action loops in your Epics. Just like working with a while loop, you need to make sure that your loop condition is solid enough to avoid infinite loops, and you want to make sure that your loop has all the other fast exits it may need.

EpicHost<State>

The last big piece of pattern is initializing all the Epics at the right point in application startup. This effort is left to a simple class known as the EpicHost<State>.

public interface IEpicHost
{
    public void Initialize();
    public IEnumerable<string> EpicNames { get; }
}

public class EpicHost<State>(
    ILogger<EpicHost<State>> logger,
    IEnumerable<IEpic<State>> epics,
    IAction action,
    IDispatch dispatch,
    ISlice<State> state)
    : IEpicHost, IDisposable
{
    private bool initialized;
    private ISubscription? subscription;
    private IScheduler scheduler = Scheduler.Default;

    public IEnumerable<string> EpicNames => epics.Select(epic => epic.Name);
    
    private IObservable<RootAction> CreateEpic(IEpic<State> epic)
    {
        logger.LogDebug("Epic {Epic} starting in {Host}", epic.Name, GetType().Name);
        return epic.Create(action.Actions, state.States, scheduler)
            .Catch(error =>
            {
                logger.LogError(error, "Error in epic {Epic}", epic.Name);
                // Restart:
                return CreateEpic(epic);
                // NOTE: You might want to include a backoff strategy for
                // restarts or other additional debugging tools here.
            });
    }

    private IObservable<RootAction> CreateEpics() =>
        Observable.Merge(epics.Select(CreateEpic));

    public void Initialize()
    {
        if (initialized)
        {
            return;
        }
        initialized = true;
        subscription = CreateEpics()
            .Subscribe(
                action => dispatch.Dispatch(action),
                error => logger.LogError(error, "Error in {Host}", GetType().Name),
                () => logger.LogError("{Host} completed", GetType().Name),
            );
    }

    public void Dispose()
    {
        GC.SuppressFinalize(this);
        subscription?.Dispose();
    }
}

public static class EpicHostExtensions
{
    public static IServiceCollection AddEpicHost<State>(
        this IServiceCollection services
    ) => services
        .AddSingleton<IEpicHost, EpicHost<State>>();
}

EpicNames can be handy for debugging and testing.

Somewhere in your application startup path where it makes the most sense:

var epicHosts = services.GetServices<IEpicHost>(); // or constructor injection
foreach (var host in epicHosts)
{
    host.Initialize();
}

To finish up our Cool Feature example registration, it’s final shape is:

public static class CoolFeatureExtensions
{
    public static IServiceCollection AddCoolFeature(this IServiceCollection services) => services
        .AddTransient<IReducer<CoolFeatureState>, CoolFeatureInitializedReducer>()
        .AddTransient<IReducer<CoolFeatureState>, CoolFeatureLoadedReducer>()
        .AddTransient<IEpic<CoolFeatureState>, CoolThingLoader>()
        .AddSlice(RootState.CoolFeatureLens)
        .AddEpicHost<CoolFeatureState>();
}

(You might want to inject an Epic Host for the RootState as well.)

Bonus: DI Registration Tests

I found it useful to test that all Reducers and Epics were registered in DI to avoid forgetting to register one (and then being confused why it wasn’t working).

It needs this helper function for Reflection:

public Func<Type, bool> ImplementsGenericInterface(Type interface)
{
    var genericInterface = interface.GetGenericTypeDefinition();
    return (Type targetType) => targetType
        .GetInterfaces()
        .Any(x => x.IsGenericInterface && x.GetGenericTypeDefinition() == genericInterface
            || x.GetInterfaces().Where(i => i.IsGenericInterface).Select(i => i.GetGenericTypeDefinition()).Contains(genericInterface));
}

With that in hand in a test context where you can test your appropriately registered DI container, you should be able to use tests that look something like (modulo your test framework and assertion framework differences):

[Fact]
public void ReducersAreRegistered()
{
    var isReducer = ImplementsGenericInterface(typeof(IReducer<>));
    var reducers = typeof(Store).Assembly.GetTypes()
        .Where(t => !t.IsAbstract && t != typeof(Slice<>) && isReducer(t));
    foreach (var reducer in reducers)
    {
        Assert.IsTrue(
            Services.Any(desc => desc.ImplementationType == reducer),
            $"Reducer {reducer.FullName} should be registered");
    }
}

[Fact]
public void EpicsAreRegistered()
{
    var isEpic = ImplementsGenericInterface(typeof(IEpic<>));
    var epics = typeof(Store).Assembly.GetTypes()
        .Where(t => !t.IsAbstract && isEpic(t));
    foreach (var epic in epics)
    {
        Assert.IsTrue(
            Services.Any(desc => desc.ImplementationType == epic),
            $"Epic {epic.FullName} should be registered");
    }
}

A Pattern for Redux with Epics in C# Dependency Injection

I think this pattern should feel built out of a lot of simple parts. I feel like most of the work in this particular pattern is just the DI “glue” to connect all the small lego parts of the pattern together.

I’ve had good success with the Epic Pattern for Redux in JS and I think we saw some good use of this pattern in the C# project we were using this pattern for (the view states and much of the business logic of a frontend built in Blazor WASM).

The reliance of C# Dependency Injection in the way this pattern uses it I think helps keep individual pieces small, focused, and easy to unit test. I wrote a lot of Reducers and Epics in this pattern, with high test coverage, and I appreciated it.

Given how much of the pattern seems to me to be most “DI glue” and I couldn’t see enough of a case to convert it to a C# library, I thought it may still be useful to at least blog the pattern for posterity, as it may be useful to other projects. In so far as this is copy and pasteable code rather than an uncopyrightable generic description of a software pattern, I believe the MIT license applies (no warranty, especially).