Proxoft.Redux.Hosting 5.1.0

dotnet add package Proxoft.Redux.Hosting --version 5.1.0                
NuGet\Install-Package Proxoft.Redux.Hosting -Version 5.1.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="Proxoft.Redux.Hosting" Version="5.1.0" />                
For projects that support PackageReference, copy this XML node into the project file to reference the package.
paket add Proxoft.Redux.Hosting --version 5.1.0                
#r "nuget: Proxoft.Redux.Hosting, 5.1.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.
// Install Proxoft.Redux.Hosting as a Cake Addin
#addin nuget:?package=Proxoft.Redux.Hosting&version=5.1.0

// Install Proxoft.Redux.Hosting as a Cake Tool
#tool nuget:?package=Proxoft.Redux.Hosting&version=5.1.0                

Redux for .NET

Predictable state container for .NET C#. Inspired by ngrx library. This library is tightly connected to System.Reactive library.

The Redux is composed of logical components

  1. State - immutable state without any logic
  2. Action - a command or even which triggers changes on state
  3. Reducer - pure function which creates a new state based on action and current state
  4. Effects - serves as a side effect (e.g. reading/writing to database, using REST, etc.)

All of these are orchestrated by Store

Getting started

Define components

State

The state is an object containing all the application data. It may be of any type (class, record, struct, primitive). The important part about it is that the state should be immutable (or it must be used as it was immutable). That means: any time a state needs to be changed a new instance with modified data must be created.

public record ApplicationState
{
    public string Message { get; init; } = "";
}
Actions

Actions are commands or events containing information (name of the action and optionally other data in the action) sent from your application to your store. They only need to implement the markup interface Redux.IAction.

public class SetMessageAction : IAction
{
    public SetMessageAction(string message)
    {
        this.Message = message;
    }

    public string Message { get; }
}

public class TriggerAction : IAction
{
}

public class ResetMessageAction : IAction
{
}
Reducers

A reducer is a pure function with ((TState)state, (IAction)action) ⇒ (TState)state signature. The reducer transform current state into the next state according to action by creating a new instance with modified data. The pure function requirement implies that the reducer cannot habe any side-effect (e.g. persisting state, fetching data from another service). The reason is that the reducer must be predictable: it must always behave the same way and its behavior (result) depends only on input arguments.

public static class ApplicationReducer
{
    public static ApplicationState Reduce(ApplicationState state, IAction action)
    {
        return action switch
        {
            SetMessageAction messageAction => state with { Message = messageAction.Message },
            ResetMessageAction => state with { Message = "" },
            _ => state
        };
    }
}
Effect

The Effect is a construct for all side-effects e.g. fetching data from REST, saving data to DB, execution of asynchronous tasks, etc. Effect may observe actions, state or both and execute corresponding actions. It also may dispatch new actions.

public class ApplicationEffect : Effect<ApplicationState>
{
    private IObservable<IAction> Effect => this.ActionStream
            .OfType<TriggerAction>()
            .Select(_ => new SetMessageAction("Triggered SetMessage"))
            ;
}
Store

The Store<TState> wires it all together. It

  • Holds application state of type TState.
  • Executes reducers any time an action is dispatched via ActionDispatcher.
  • Publishes update state
  • Publishes actions and updated states to the effect

Store store = StoreHelper.Create(ApplicationReducer.Reduce, effects: new ApplicationEffect());
store.Initialize(new ApplicationState());

Use the store

Dispatch actions


store.Dispatcher.Dispatch(new TriggerAction());
store.Dispatcher.Dispatch(new ResetMessageAction());

Note: it is possible to register and inject own implementation of IActionDispatcher using DI. (see proxoft.redux.hosting package)

Subscribe to state changes


store.StateStream
    .Select(state => {
        Console.WriteLine(state.Message);
    });

Builder

use the proxoft.redux.hosting package


services.AddRedux<ApplicationState>(
            builder =>
            {
                builder
                    .UseScheduler(Scheduler.Default)
                    .UseJournaler<ActionJournaler>()
                    .UseExceptionHandler<DefaultExceptionHandler>()
                    .UseReducer(ApplicationReduce.Reduce)
                    .AddEffects(typeof(ApplicationState).Assembly);
            });

Other features

In some scenarios it might be useful to suppress actions that have been dispatched, for example repetitive dispatch of an action or concurrent state changes causing dispatched action to become invalid.


public record ApplicationState
{
    public bool ProcessRunning { get; init; }
    public string LastMessage { get; init; } = "";
}

public class StartProcessAction : IAction
{
}

public class StopProcessAction : IAction
{
}

public class WarningAction(string message) : IAction
{
    public string Message { get; } = message;
}

public static class ApplicationReducer
{
    public static ApplicationState Reduce(ApplicationState state, IAction action)
    {
        return action switch
        {
            StartProcessAction => state with { ProcessRunning = true },
            StopProcessAction => state with { ProcessRunning = false },
            WarningAction a => state with { LastMessage = a.Message },
            _ => state
        };
    }
}

public class ExternProcessRunner
{
    private bool _started;

    public void Start()
    {
        if (_started)
        {
            throw new Exception("cannot start");
        }

        _started = true;
    }

    public void Stop()
    {
        if (_started)
        {
            throw new Exception("cannot stop");
        }

        _started = false;
    }
}

public class ProcessEffect(ExternProcessRunner runner) : Effect<ApplicationState>
{
    private readonly ExternProcessRunner _runner = runner;

    private IObservable<Unit> OnStart => this.ActionStream
            .OfType<StartProcessAction>()
            .Do(_ => _runner.Start())
            .SelectVoid()
            ;

    private IObservable<Unit> OnStop => this.ActionStream
            .OfType<StopProcessAction>()
            .Do(_ => _runner.Stop())
            .SelectVoid()
            ;
}

public static class ActionGuard
{
    public static IAction Validate(IAction action, ApplicationState state)
    {
        return action switch
        {
            StartProcessAction when state.ProcessRunning => new WarningAction("process already started"),
            StopProcessAction when state.ProcessRunning == false => new WarningAction("process already stopped"),
            _ => action
        };
    }
}

// ...

Store<ApplicationState> store = StoreHelper.Create<ApplicationState>(
    ApplicationReducer.Reduce,
    ActionGuard.Validate,
    effects: new ProcessEffect(new ExternProcessRunner())
);

store.Initialize(new ApplicationState());

store.Dispatcher.Dispatch(new StartProcessAction());
store.Dispatcher.Dispatch(new StartProcessAction()); // will be changed to WarningAction

store.Dispatcher.Dispatch(new StopProcessAction());
store.Dispatcher.Dispatch(new StopProcessAction());  // will be changed to WarningAction

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 was computed.  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. 
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.1.0 23,913 6/25/2024
5.0.1 7,511 11/7/2023
4.3.0 238 8/15/2023
4.2.0 175 5/25/2023
4.1.0 17,229 6/8/2022
4.0.3 564 5/18/2022
4.0.2 419 5/17/2022
4.0.1 417 5/17/2022
4.0.0 422 5/16/2022
3.1.2 921 3/4/2022
3.1.1 433 2/27/2022
3.1.0 407 2/27/2022
3.0.1 406 2/26/2022
3.0.0 425 2/26/2022
2.4.0 850 2/11/2022
2.3.0 470 10/28/2021
2.2.0 1,075 9/14/2021
2.1.1 463 7/19/2021
2.1.0 319 7/19/2021
2.0.0 323 7/12/2021
1.0.3 1,146 1/13/2021
1.0.2 333 1/13/2021