Skip to main content

License: MIT NuGet Version

StatePulse.NET

Official Documentation

StatePulse.NET is a precision-tuned state and action management system that balances high-performance fire-and-forget operations with optional, internally controlled execution order when explicitly required. It enables anti-duplication chaining for critical flows, preventing race conditions and ensuring consistent outcomes even under rapid user input or concurrent triggers. Its internal tracking infrastructure provides near-zero overhead cancellation and dispatch control, drastically reducing inconsistency. At the same time, it preserves the flexibility of traditional untracked state management, letting developers selectively enforce order and reliability without compromising overall responsiveness or introducing global locks.

✨ Features

  • Fast Fire-and-Forget — Executes actions immediately even tracked action are fire-and-forget.
  • 🛡 Anti-Duplicate Dispatching — Prevents redundant or overlapping actions that can cause race condition state inconsistency.
  • 🔍 Validator System — Supports multiple action validators for modular and reusable rule enforcement.
  • 🧪 Synchronous Debug Mode — Optional lockstep mode for testing, diagnostics, and Task.WhenAll pipelines.
  • 🧵 DispatchTracker — High-performance cancellation and deduplication logic via optimized concurrent tracking.

🚀 State Management with Zero Boilerplate and Zero Compromises

  • Lazy State Access Model: Inject IStatePulse directly into your Blazor component and call StateOf<TState>(()=>this, TaskMethod) to get scoped state access.
  • Component-Scoped Event Listening: Automatically registers event listeners only for that component, ensuring StateHasChanged() is called exclusively on components subscribed to state changes.
  • No Base Classes or Global Event Listeners: Avoids global re-renders and boilerplate base class inheritance, giving you fine-grained control over component rendering and event subscription without forcing you into base classes.
  • Automatic Listener Disposal: Event listeners are automatically tracked and disposed with the component lifecycle, preventing memory leaks and dangling references.
  • Transient IStatePulse Service: Each component gets its own IStatePulse instance, isolating event subscriptions and making state updates scoped and efficient.

📦 Installation & Setup

Install-Package StatePulse.Net

dotnet add package StatePulse.Net

services.AddStatePulseServices(o =>
{
o.ScanAssemblies = new Type[] { typeof(Program) };
});

🧭 How It Works

Define Actions:


// IAction { }
// ISafeAction { } // Cannot be dispatched unsafely

public record ProfileCardDefineAction : IAction
{
public string? TestData { get; set; }
}

Define Actions Validator (Optional):

/*
You are not required to create have an action validator but it is very useful when you have business logic that conditionally only contionally fires.
When validation fails it ignores the dispatch and move on.
*/
internal class ProfileCardDefineActionValidator : IActionValidator<ProfileCardDefineAction>
{
public void Validate(ProfileCardDefineAction action, ref ValidationResult result)
{
if (action.TestData == "Error")
result.AddError("ErrorName", "Name Cannot be Error");
}
}

Define Effect:


internal class ProfileCardDefineEffect : IEffect<ProfileCardDefineAction>
{

public ProfileCardDefineEffect()
{
}
public async Task EffectAsync(ProfileCardDefineAction action, IDispatcher dispatcher)
{
var random = new Random();
int value = random.Next(100, 1001); // Upper bound is exclusive, so use 1001
await Task.Delay(value);
var myProfile = new UserResponse();
await dispatcher.Prepare(() => new ProfileCardDefineResultAction(action.TestData ?? myProfile.Name, myProfile.Picture, myProfile.Id))
.DispatchAsync();
}

}


Define Reducer:

internal class ProfileCardDefineResultReducer : IReducer<ProfileCardState, ProfileCardDefineResultAction>
{
public Task<ProfileCardState> ReduceAsync(ProfileCardState state, ProfileCardDefineResultAction action)
=> Task.FromResult(state with
{
LastUpdate = DateTime.UtcNow,
ProfileId = action.Id,
ProfileName = action.Name,
ProfilePicture = action.Picture
});
}

Define StateFeature:

public record ProfileCardState : IStateFeature
{
public string? ProfileName { get; set; }
public string? ProfilePicture { get; set; }
public string? ProfileId { get; set; }
public DateTime LastUpdate { get; set; } = DateTime.UtcNow;
}

Trigger Dispatch:

var dispatcher = ServiceProvider.GetRequiredService<IDispatcher>();
var stateAccessor = ServiceProvider.GetRequiredService<IStateAccessor<ProfileCardState>>();
await dispatcher.Prepare<ProfileCardDefineAction>().With(p => p.TestData, name)
.DispatchAsync();

// You can Capture the validation in case of failure, only call if validators exist.
ValidationResult? validation = default;
await dispatcher.Prepare<ProfileCardDefineAction>().With(p => p.TestData, name)
.HandleActionValidation(p => validation = p)
.DispatchAsync();

// You can trigger synchronously... this will await the whole pipeline, otherwise you just await until action is send to dispatch pool.
await dispatcher.Prepare<ProfileCardDefineAction>().With(p => p.TestData, name)
.Sync()
.DispatchAsync();

// if the action is implementing ISafeState, the dispatch will always run asSafe=true but an action not implementing ISafeAction will
// have the option to run asSafe or not...
await dispatcher.Prepare<ProfileCardDefineAction>().With(p => p.TestData, name)
.DispatchAsync(true);

Important Notes

  • Rule of thumb is always await dispatch calls, avoiding to do so can cause inconsistency for safe dispatch mode..
  • ISafeAction implementations are always dispatched safely, ignoring unsafe flags.
  • synchronous is an anti-pattern of statemanement use it sparingly; it is primarily for debugging or specific scenarios requiring full completion before continuation.

Access State:

var stateAccessor = ServiceProvider.GetRequiredService<IStateAccessor<ProfileCardState>>();

Blazor Example Usage

using StatePulse.Net;

public partial class CounterView : ComponentBase
{

// METHOD 1:
[Inject] public IStatePulse PulseState { get; set; } = default!; // Handles State Accessor

// This is for convienience always use this method or directly PulseState.StateOf<CounterState>(this).Value
// Never assign State Instance variable as it will not update...
// Never use lambda it will throw exception as WeakREference is fundamatally flawed and disposes of lambda even when its object is alive.
private CounterState state => PulseState.StateOf<CounterState>(()=>this, OnUpdate);

private async Task OnUpdate() => await InvokeAsync(StateHadChanged);

// METHOD 2:
// Inject direct state but injecting the state directly requires you to handle onchanged events by sub/unsub in lifecycle
// Or to create a basecomponent system similar to other state management systems.
[Inject] public IStateAccessor<CounterState> State { get; set; } = default!;


}