StateManagementSharp 2.0.1
dotnet add package StateManagementSharp --version 2.0.1
NuGet\Install-Package StateManagementSharp -Version 2.0.1
<PackageReference Include="StateManagementSharp" Version="2.0.1" />
<PackageVersion Include="StateManagementSharp" Version="2.0.1" />
<PackageReference Include="StateManagementSharp" />
paket add StateManagementSharp --version 2.0.1
#r "nuget: StateManagementSharp, 2.0.1"
#:package StateManagementSharp@2.0.1
#addin nuget:?package=StateManagementSharp&version=2.0.1
#tool nuget:?package=StateManagementSharp&version=2.0.1
StateManagementSharp
StateManagementSharp is a lightweight, dependency-injection-native library for centralized, app-wide state in .NET applications. It organizes shared state into a Store composed of feature Modules, and changes flow through an explicit Action → Mutation pipeline, so state transitions stay predictable and easy to follow.
It is conceptually inspired by store/module/action/mutation architectures (such as Vuex and NGXS), but it is a pure .NET library: no JavaScript runtime, no Vue/Angular dependency, and it builds directly on Microsoft.Extensions.DependencyInjection.
See CHANGELOG.md for release history.
What it is (and what it is not)
It is a small, structured container for state that is shared across many parts of an app — the kind of state that does not belong to a single view or view model (session/profile, app context, feature-level domain state) — plus a disciplined way to read and change it.
It is not a full reactive UI framework or an Rx pipeline. It does, however, raise change notifications: both Store and each ModuleBase expose a StateChanged event and an Observe(selector, onChanged) API (with distinct-until-changed), and the companion StateManagementSharp.Maui package turns those into automatic, UI-thread-marshaled bindings. The UI layer itself stays yours.
When to use it
- You have app-wide or cross-view-model state and want a single, predictable place for it.
- You want explicit, traceable state transitions: actions describe intent, and mutations are the only code that changes state.
- You already use
Microsoft.Extensions.DependencyInjectionand want state management that fits naturally into it. - You are building .NET MAUI, ASP.NET Core, worker, or console apps and want the same model across all of them.
When NOT to use it
- You only need per-view-model local state — the MVVM Toolkit is simpler and sufficient.
- You want a full reactive framework (Rx streams, operators, effects) — StateManagementSharp signals that state changed and lets you observe selectors, but it does not model event pipelines.
- You need Redux DevTools, time-travel debugging, or effects middleware now — consider Fluxor.
- You want reactive streams over state — consider ReactiveUI.
How it compares
| Option | Focus | Relationship to StateManagementSharp |
|---|---|---|
| CommunityToolkit.Mvvm (MVVM Toolkit) | Observable properties, commands, and messaging per view model | Complementary, not a replacement. Keep using it for view-model state and commands; use StateManagementSharp for the shared, app-wide state those view models read from. |
| Fluxor | Flux/Redux for .NET, strong in Blazor, with effects and Redux DevTools | More feature-complete and Blazor-proven. StateManagementSharp is smaller, module-oriented, and aimed at the general .NET/MAUI case; it does not (yet) offer DevTools or effects. |
| ReactiveUI | Reactive (Rx) MVVM and event pipelines | A different mental model. Choose StateManagementSharp if you want a store/module structure without adopting Rx. |
| A custom singleton service + DI | Whatever you hand-roll | StateManagementSharp is essentially a structured version of that pattern: modules, typed Dispatch/Commit, and a single mutation path. If one shared service is all you need, a plain singleton may be enough; the structure pays off as shared state grows. |
Using it in .NET MAUI
MAUI apps often need state that outlives a single page and is shared by several view models — session and profile data, an app-context/busy flag, feature state. StateManagementSharp gives that state a single home and a predictable update path, registered through the same DI container MAUI already uses.
With the StateManagementSharp.Maui package, a view model derives from StoreBindableObject and binds selectors to BindableValue<T> properties; changes are pushed to the UI thread automatically, with no manual refresh (see the MAUI example below).
Features
- Store-based application state with
Store<TRootState>. - Feature modules through
ModuleBase<TState, TRootState>. - Asynchronous actions through
IAction<TState, TRootState>. - State updates through
IMutation<TState, TPayload>. - Typed module dispatch with
Dispatch<TAction>()and typed commits withCommit<TMutation, TPayload>(payload). - Name-based
Dispatch(actionName)andCommit(mutationName, payload)where supported. - Change notification via
StateChangedevents onStore/ModuleBase, plusObserve(selector, onChanged)with distinct-until-changed. - DI setup with
AddStateManagementSharp(params Assembly[])and assembly scanning for concrete action types. - First-class .NET MAUI integration via the companion
StateManagementSharp.Mauipackage (UseStateManagementSharp<TStore>,StoreBindableObject,BindableValue<T>). - Runs on .NET MAUI, ASP.NET Core / Web API, worker services, and console apps.
- Built directly on
Microsoft.Extensions.DependencyInjection.
Installation
StateManagementSharp ships as two packages, and the split is intentional — not an accidental complication:
StateManagementSharp— the UI-agnostic core, usable from any .NET app.StateManagementSharp.Maui— an optional .NET MAUI adapter (StoreBindableObject,BindableValue<T>,UseStateManagementSharp<TStore>()) that depends on and includes the core.
Non-MAUI apps (console, worker, Web API, tests, or any app that owns its own UI binding) — install the core package:
dotnet add package StateManagementSharp --version 2.0.1
.NET MAUI apps — install the adapter only; it brings the core in transitively, so you do not install the core separately:
dotnet add package StateManagementSharp.Maui --version 2.0.1
The core multi-targets netstandard2.0, net8.0, and net10.0; the adapter targets the MAUI platforms (net10.0-android, net10.0-ios, net10.0-maccatalyst). The sample app in this repository is an example and is not part of either package.
Which package should I install?
| Scenario | Install | Recommended setup |
|---|---|---|
| Console / worker / Web API / tests | StateManagementSharp |
services.AddStateManagementSharp(typeof(MyStore).Assembly) + register the store |
| .NET MAUI with binding helpers | StateManagementSharp.Maui |
builder.UseStateManagementSharp<MyStore>() |
Migrating from 1.x to 2.0
2.0 is a breaking release. The marker interfaces were renamed to the conventional I-prefix, which also removes the long-standing collision between Action<TState, TRootState> and System.Action<T1, T2> — you no longer need to fully qualify StateManagementSharp.Action.
| 1.x | 2.0 |
|---|---|
State |
IState |
RootState |
IRootState |
Action / Action<TS, TR> |
IAction / IAction<TS, TR> |
Mutation / Mutation<S, P> |
IMutation / IMutation<S, P> |
Module |
IModule |
StateManagementSharp.Action<...> (qualified to dodge System.Action) |
IAction<...> (no qualification needed) |
ActionContext<TS, TR>, the factory interfaces (ActionFactory, MutationFactory, StateFactory), Store<TR>, and ModuleBase<TS, TR> keep their names. A whole-word replace of the five marker names in type positions covers almost every project; then drop any StateManagementSharp. qualifier in front of Action.
New in 2.0: subscribe to Store.StateChanged / ModuleBase.StateChanged, or call Observe(selector, onChanged) for fine-grained updates. In MAUI, add the StateManagementSharp.Maui package and derive view models from StoreBindableObject instead of hand-writing INotifyPropertyChanged.
Core setup (non-MAUI)
In a console, worker, Web API, test, or any non-MAUI app, register the services and your store on the DI container directly:
using Microsoft.Extensions.DependencyInjection;
builder.Services.AddStateManagementSharp(typeof(InternalStore).Assembly);
builder.Services.AddSingleton<InternalStore>();
AddStateManagementSharp(...) is available through the standard DI namespace Microsoft.Extensions.DependencyInjection. It registers the library factories and scans the assemblies you pass for concrete action types (resolved from DI). Pass the assembly that contains your store, its modules, actions, and mutations — typically typeof(InternalStore).Assembly, because discovery expects them to live together. The store itself is not auto-registered, so add it yourself (a singleton for app-wide state):
builder.Services.AddStateManagementSharp(scanAssemblies); // scanAssemblies must include the store's assembly
builder.Services.AddSingleton<InternalStore>();
Minimal Example
The following example shows the core pattern: define a root state, a module state, a module, a mutation, an action, and a store.
using Microsoft.Extensions.DependencyInjection;
using StateManagementSharp;
public sealed class AppState : IRootState
{
private AppStore Store { get; }
public CounterState Counter => Store.Counter.State!;
public AppState(AppStore store)
{
Store = store;
}
}
public sealed record CounterState(int Value) : IState;
public sealed class CounterModule : ModuleBase<CounterState, AppState>
{
public CounterModule(AppStore store, MutationFactory mutationFactory, ActionFactory actionFactory)
: base(store, mutationFactory, actionFactory)
{
}
public override void CreateState()
{
State = new CounterState(0);
}
public override void DisposeState()
{
State = null;
}
}
public sealed class IncrementMutation : IMutation<CounterState, int>
{
public CounterState Apply(CounterState state, int amount)
{
return state with { Value = state.Value + amount };
}
}
public sealed class IncrementAction : IAction<CounterState, AppState>
{
public Task Execute(ActionContext<CounterState, AppState> context, object? payload)
{
var amount = payload is int value ? value : 1;
context.Commit<IncrementMutation, int>(amount);
return Task.CompletedTask;
}
}
public sealed class AppStore : Store<AppState>
{
public CounterModule Counter { get; private set; } = null!;
private MutationFactory MutationFactory { get; }
private ActionFactory ActionFactory { get; }
public AppStore(MutationFactory mutationFactory, ActionFactory actionFactory)
{
MutationFactory = mutationFactory;
ActionFactory = actionFactory;
State = new AppState(this);
}
public override void InitializeStore()
{
BindModules();
BootstrapModules();
}
protected override void BindModules()
{
Counter = new CounterModule(this, MutationFactory, ActionFactory);
Modules = [Counter];
}
public override void InitState()
{
State = new AppState(this);
}
}
Use the store through DI:
var services = new ServiceCollection();
services.AddStateManagementSharp(typeof(AppStore).Assembly);
services.AddSingleton<AppStore>();
await using var provider = services.BuildServiceProvider();
var store = provider.GetRequiredService<AppStore>();
store.InitializeStore();
await store.Counter.Dispatch<IncrementAction>(5);
var value = store.State?.Counter.Value;
Reacting to state changes
Every commit raises StateChanged on the owning module and on the store. For fine-grained updates, Observe a selector — it emits the current value immediately, then again only when the projected value changes:
store.InitializeStore();
// Fires now with the initial value, then on every distinct change.
using var subscription = store.Counter.Observe(
state => state.Value,
value => Console.WriteLine($"Counter is now {value}"));
// Any module commit re-evaluates a store-level selector:
store.StateChanged += (_, _) => Console.WriteLine("Something changed");
await store.Counter.Dispatch<IncrementAction>(5); // prints "Counter is now 5"
Callbacks run synchronously on the thread that committed; UI-thread marshaling is handled by the StateManagementSharp.Maui package (below). Dispose the Observe subscription to stop receiving updates.
.NET MAUI Usage
With the StateManagementSharp.Maui package, the recommended setup is a single call on the app builder:
using Microsoft.Maui.Hosting;
builder
.UseMauiApp<App>()
.UseStateManagementSharp<InternalStore>();
UseStateManagementSharp<TStore>() is available through the standard MAUI namespace Microsoft.Maui.Hosting. It does, in one step, everything the core setup does: it registers the StateManagementSharp core services, scans an assembly for actions (defaulting to typeof(TStore).Assembly), and registers TStore as a singleton. So in a MAUI app you don't also call builder.Services.AddStateManagementSharp(...) and AddSingleton<InternalStore>() for the same store — that is already covered. (Pass explicit assemblies, e.g. UseStateManagementSharp<InternalStore>(extraAssembly), only when your actions live outside the store's assembly.)
Derive the view model from StoreBindableObject and bind selectors to BindableValue<T> properties. Changes are marshaled to the UI thread automatically — no manual refresh, no hand-written INotifyPropertyChanged:
public sealed class MainPageViewModel : StoreBindableObject
{
public MainPageViewModel(InternalStore store, IDispatcher dispatcher)
: base(dispatcher)
{
store.InitializeStore();
FirstName = Bind(store.ProfileModule, s => s.FirstName);
IsBusy = Bind(store.AppContextModule, s => s.IsBusy);
LoadProfileCommand = new Command(async () => await store.ProfileModule.Dispatch<LoadProfileAction>());
}
public BindableValue<string?> FirstName { get; }
public BindableValue<bool> IsBusy { get; }
public ICommand LoadProfileCommand { get; }
}
Bind the .Value path in XAML:
<Label Text="{Binding FirstName.Value}" />
<Label Text="{Binding IsBusy.Value}" />
BindableValue<T> implements INotifyPropertyChanged (woven at compile time by Fody, inside the package), so the view updates whenever the selected value changes.
Bind vs Observe
Bind(selector)— for values the UI displays, raw or derived/computed. It returns aBindableValue<T>that recomputes after every commit and is marshaled to the UI thread for you. Use it for anything the view binds to.Observe(selector, onChanged)— for reacting to state changes with a side-effect (logging, navigation, analytics, refreshing non-bound data). It runs on the thread that committed, so marshal yourself if you touch the UI. Both are distinct-until-changed (EqualityComparer<T>.Default), so identical values don't fire.
// Bind: a DERIVED value shown by the UI (no such field exists in the state).
Summary = Bind(store.ProfileModule, s => s.IsLoaded ? $"{s.FirstName} {s.LastName}" : "—");
// Observe: a side-effect reaction (here, appending to a log).
_subscription = store.ProfileModule.Observe(
s => s.IsLoaded,
loaded => AppendLog(loaded ? "profile loaded" : "profile cleared"));
The sample app (SAMPLE/StateManagementSharp.Sample.Maui) demonstrates both side by side, with derived Bind values and a visible Observe reaction log.
Web API And Console Usage
StateManagementSharp uses Microsoft DI, so the setup is the same in ASP.NET Core, worker services, and console apps:
services.AddStateManagementSharp(typeof(AppStore).Assembly);
services.AddSingleton<AppStore>();
Register the store as a singleton for app-wide state. Scoped stores are also possible when the application owns the lifecycle and state boundaries.
Sample App
The MAUI sample is located at:
SAMPLE/StateManagementSharp.Sample.Maui
It demonstrates:
Store/InternalStore.csandStore/InternalState.cs.AppContextandProfilemodules.- Actions and mutations for initializing app context, toggling busy state, loading a profile, and clearing a profile.
- DI registration in
MauiProgram.cs. - Store usage from
MainPageViewModel.
Build
dotnet restore StateManagementSharp.sln
dotnet build StateManagementSharp/StateManagementSharp.csproj -c Release
dotnet build StateManagementSharp.Maui/StateManagementSharp.Maui.csproj -c Release -p:MauiVersion=<installed-maui-version>
dotnet build SAMPLE/StateManagementSharp.Sample.Maui/StateManagementSharp.Sample.Maui.csproj -f net10.0-android -c Debug -p:MauiVersion=<installed-maui-version>
Pass -p:MauiVersion matching your installed MAUI workload (for example 10.0.20) when building the MAUI package or sample. Building StateManagementSharp.Maui for the iOS and MacCatalyst target frameworks requires macOS.
Tests
dotnet test StateManagementSharp.Tests/StateManagementSharp.Tests.csproj -c Release
Requirements
- .NET 8.0 or later, or any target compatible with .NET Standard 2.0.
Microsoft.Extensions.DependencyInjection.- The .NET MAUI workload to build the sample app.
The package multi-targets netstandard2.0, net8.0, and net10.0, so it can be referenced from .NET Framework 4.6.2+, modern .NET, and .NET MAUI projects.
Roadmap
Shipped in 2.0:
- Change notification (
StateChanged+Observe) and automatic, UI-thread-marshaled binding via theStateManagementSharp.Mauipackage (bindable adapters woven with Fody).
Planned, but not available yet:
- Source-generated action/mutation registration to replace runtime reflection (
MakeGenericMethod), enabling iOS full-AOT/trimming. - Logging/diagnostics and Redux-style middleware (DevTools).
- Relaxing the same-assembly discovery constraint.
Known Limitations
- StateManagementSharp is not a complete Vuex or NGXS clone.
- No built-in persistence, time-travel, or DevTools support.
- iOS full-AOT / trimming is not supported yet. Name-based
Dispatch/Commituse runtime reflection (Assembly.GetTypes(),MakeGenericMethod) that fails under full AOT/trimming; the typedDispatch<TAction>/Commit<TMutation, TPayload>andObservepaths are AOT-safe. The fix (source-generated registration) is planned; the sample keepsPublishTrimmed/RunAOTCompilationdisabled. - Store lifecycle and scoping depend on the app's DI registration; register the store as a singleton for app-wide state.
- Action and mutation discovery scans the store's own assembly, so the store, its modules, actions, and mutations are expected to live in the same assembly.
- On
netstandard2.0, using C#record/init-only state types requires anIsExternalInitpolyfill in the consumer project (not needed onnet8.0/net10.0).
License
StateManagementSharp is licensed under the MIT License.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. 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 was computed. 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 is compatible. 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. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
| .NET Core | netcoreapp2.0 was computed. netcoreapp2.1 was computed. netcoreapp2.2 was computed. netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.0 is compatible. netstandard2.1 was computed. |
| .NET Framework | net461 was computed. net462 was computed. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 was computed. net481 was computed. |
| MonoAndroid | monoandroid was computed. |
| MonoMac | monomac was computed. |
| MonoTouch | monotouch was computed. |
| Tizen | tizen40 was computed. tizen60 was computed. |
| Xamarin.iOS | xamarinios was computed. |
| Xamarin.Mac | xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETStandard 2.0
- Microsoft.Extensions.DependencyInjection (>= 10.0.8)
-
net10.0
- Microsoft.Extensions.DependencyInjection (>= 10.0.8)
-
net8.0
- Microsoft.Extensions.DependencyInjection (>= 10.0.8)
NuGet packages (1)
Showing the top 1 NuGet packages that depend on StateManagementSharp:
| Package | Downloads |
|---|---|
|
StateManagementSharp.Maui
.NET MAUI integration for StateManagementSharp: automatic, UI-thread-marshaled binding to store state via selectors, using Fody-woven bindable adapters. |
GitHub repositories
This package is not used by any popular GitHub repositories.
2.0.1 finalizes the 2.0 package distribution: restores setup extension methods to the intended framework namespaces, clarifies core/MAUI setup documentation, and keeps the reactive state-management behavior unchanged from 2.0.0.