Dux 0.0.3

dotnet add package Dux --version 0.0.3
                    
NuGet\Install-Package Dux -Version 0.0.3
                    
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="Dux" Version="0.0.3">
  <PrivateAssets>all</PrivateAssets>
  <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Dux" Version="0.0.3" />
                    
Directory.Packages.props
<PackageReference Include="Dux">
  <PrivateAssets>all</PrivateAssets>
  <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Dux --version 0.0.3
                    
#r "nuget: Dux, 0.0.3"
                    
#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.
#:package Dux@0.0.3
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Dux&version=0.0.3
                    
Install as a Cake Addin
#tool nuget:?package=Dux&version=0.0.3
                    
Install as a Cake Tool

Dux

NuGet version of Dux NuGet version of Dux.Abstractions NuGet version of Dux.Blazor

Status of CI workflow Status of release workflow Latest release version

A source-generated Redux/Flux state management library for .NET.

Features

  • Fully source-generated implementation
  • Visibility of usage and calls into state store methods
  • Step-through debugging of dispatched actions
  • Minimal boilerplate

Installation

The Dux library makes use of several packages.

Add the main packages to your project:

dotnet add package Dux
dotnet add package Dux.Abstractions

If you are making use of Blazor, your project should also reference the Blazor package for tooling to make integration easier with components:

dotnet add package Dux.Blazor

Limitations

This library is new and so does not have the full suite of features and functionality that similar libraries have. Please be patient and understanding.

Some limitations include:

  • At least one state object must be defined before the dependency injection extension method is generated.
  • All store definitions must be defined within the 'root' project of your application, definitions within other projects referenced by the root will not be detected by the generator.
  • Redux dev tool integrations are not presently supported.
  • Middleware is not presently supported.

Usage

To maintain strong typing, the source generator operates against a set of interfaces as opposed to tagging with attributes, which is different to similar libraries.

Store definitions make use of these interfaces:

  • IDuxState
  • IDuxAction
  • IDuxReducer

Interfaces used to consume the store via dependency injection include:

  • IDuxStore<TYourState>
  • IDuxDispatcher

1. Define your state

State stores are defined by creating a class or record type with the IDuxState marker interface.

For the best experience, state should be immutable. Only reducers acting on actions are allowed to mutate state, making use of record types and immutable variants of collections (like arrays and immutable/readonly lists) help to enforce this.

Note that state needs to have a parameterless constructor, which means it should also not make use of required properties. Elements within the main state object, of course, do not have this requirement and may be implemented as desired.

public record TodoState : IDuxState
{
    public ImmutableList<Todo> Todos { get; init; } = ImmutableList<Todo>.Empty;
}

public record Todo 
{
    public required Guid Id { get; init; }
    public requried string Value { get; init; }
}

2. Define actions

You may define your actions any way you like, but they need to make use of the IDuxAction marker interface.

public record AddTodoAction(Todo Todo) : IDuxAction;
public record RemoveTodoAction(Guid TodoId) : IDuxAction;

3. Create reducers

Reducers are defined by implementing the IDuxReducer interface, with any number of static or non-static methods. The individual reducers are detected based on the parameters in each method: the first is the state to mutate (which will be passed in the call), the second is the action. The method must return the mutated state, which becomes the current state.

Any reducer may handle any dispatched action on behalf of any state.

public class TodoReducer : IDuxReducer
{
    // Methods are discovered by parameter types
    public TodoState HandleAddTodo(TodoState state, AddTodoAction action)
    {
        return state with { Todos = state.Todos.Add(action.Todo) };
    }

    public static TodoState HandleRemoveTodo(TodoState state, RemoveTodoAction action)
    {
        return state with { 
            Todos = state.Todos.Remove(
                state.Todos.First(t => t.Id == action.TodoId)
            )
        };
    }
}

4. Create effects (optional)

Effects may optionally be defined to perform async operations. Effects fire after reducers have completed.

Much like reducers, the individual effects within an IDuxEffect are detected based on the parameters of each method: the first is the action, and the second is an optional cancellation token. If you don't need or want a cancellation token, don't include it in the parameters. Effects may not be static.

The cancellation token comes from the point of dispatch - so if you dispatch an action in a Blazor page, and you navigate away, it will be canceled. Pass CancellationToken.None explicitly when dispatching if you want the effect to continue executing in this case.

To dispatch other events as a result of an effect, use dependency injection to get a reference to the IDuxDispatcher. To get the current state, the same applies - use dependency injection to get a reference to the relevant IDuxStore<TState>.

Any effect may handle any action.

public class TodoEffect : IDuxEffect
{
    private readonly IMyTelemetryApi _telemetry;
    
    public TodoEffect(IMyTelemetryApi telemetry)
    {
        _telemetry = telemetry;
    }

    public async ValueTask HandleAddTodo(AddTodoAction action, CancellationToken cancellationToken)
    {
        await _telemetry.TrackAsync($"Todo added: {action.Todo.Id}", cancellationToken);
    }
}

5. Register services

services.AddDux(); // Registers everything automatically

Note that at least one IDuxState must be defined within the project before the extension method is generated.

6. Use in your application

Resolve services like the dispatcher (IDuxDispatcher) and any state (IDuxStore<TYourState>) as you might expect, with dependency injection.

The Value of any IDuxStore<T> will contain the most up-to-date state object each time it is read.

To trigger your application to read the state again when it changes, you may hook into the StateChanged event on any injected IDuxStore<T>, or the StateChanged event on the IDuxDispatcher for all events across all stores.

Additionally, there is an option to await all processing on a dispatched action by using the DispatchAsync overload: DispatchAsync(action, waitForCompletion: true, cancellationToken). If waitForCompletion is false (including when using the other overloads without it), then the action is processed in the background, the method returning as soon as it is in the processing queue.

public class TodoComponent
{
    private readonly IDuxDispatcher _dispatcher;
    private readonly IDuxStore<TodoState> _todoStore;

    public TodoComponent(
        IDuxDispatcher dispatcher,
        IDuxStore<TodoState> todoStore)
    {
        _dispatcher = dispatcher;
        _todoStore = todoStore;
    }

    public async ValueTask AddTodo(Todo todo, CancellationToken cancellationToken)
    {
        // Enqueue into the background
        await _dispatcher.DispatchAsync(new AddTodoAction(todo), cancellationToken);
        
        // Or: wait for all reducers and effects to complete before returning
        await _dispatcher.DispatchAsync(new AddTodoAction(todo), waitForCompletion: true, cancellationToken);
    }
}

6.1 Use in your Blazor application

When using the library in Blazor, the Dux.Blazor package provides some base components to manage integration.

These base components are:

  • DuxComponent - provides automatic render updates based on state updates
  • CancellableDuxComponent - extends DuxComponent with a cancellation token for use when dispatching, in case you don't already have an implementation for cancellable components

The component bases also include a dispatcher property, since they also use it internally anyway.

Note that when using these components, if you override OnInitialized, be sure to call base.OnInitialized() so that these components may perform their own initialization.

@page "/"
@inherits CancellableDuxComponent // <- The base component

@inject IDuxStore<TodoState> TodoStore

<p>You have @TodoStore.Value.Todos.Count todos on your list</p>
<button @onclick="HandleAddTodo">Add random</button>

@code
{
    private async Task HandleAddTodo()
    {
        var newTodo = new Todo { Id = Guid.NewGuid(), Value = "Something" };
        await Dispatcher // <- From the inherited `DuxComponent`
            .DispatchAsync(
                new AddTodoAction(newTodo),
                CancellationToken); // <- From the inherited `CancellableDuxComponent`
    }
}
There are no supported framework assets in this 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
0.0.3 94 4/26/2025
0.0.2 91 4/26/2025
0.0.1 93 4/26/2025