Middleware

Extend store behavior with custom middleware for cross-cutting concerns like logging, validation, and custom logic.

What is Middleware?

Middleware intercepts state updates before and after they happen, allowing you to add cross-cutting concerns without modifying your state logic. Think of it as a pipeline that wraps around every state update.

Key Use Cases
  • Logging: Track state changes for debugging
  • DevTools Integration: Time-travel debugging with Redux DevTools
  • Persistence: Auto-save state to LocalStorage/SessionStorage
  • Validation: Enforce business rules before updates
  • Analytics: Track user actions and state changes
  • Notifications: Trigger side effects on specific actions

IMiddleware Interface

All middleware implements the IMiddleware<TState> interface with two hooks:

IMiddleware<TState>
public interface IMiddleware<TState> where TState : notnull
{
    // Called BEFORE state is updated
    Task OnBeforeUpdateAsync(TState currentState, string? action);

    // Called AFTER state has been updated
    Task OnAfterUpdateAsync(TState newState, string? action);
}
Important Notes
  • Middleware executes in the order it was registered
  • Both hooks are asynchronous to support I/O operations
  • The action parameter is optional and can be null
  • Middleware should not modify state directly

Built-in Middleware

The library includes several production-ready middleware implementations:

🛠️ DevToolsMiddleware

Integrates with Redux DevTools browser extension for time-travel debugging, state inspection, and action replay.

💾 PersistenceMiddleware

Automatically saves state to LocalStorage or SessionStorage on updates and restores it on app load.

📝 LoggingMiddleware

Logs state changes to the console or your logging provider for debugging and monitoring.

Quick Setup

Use .WithDefaults(sp, "StoreName") to automatically add DevTools and Logging middleware:

builder.Services.AddStoreWithUtilities(
    new AppState(),
    (store, sp) => store.WithDefaults(sp, "MyApp"));  // Includes DevTools + Logging

Creating Custom Middleware

Implement IMiddleware<TState> to create your own middleware:

1

Define the Middleware Class

LoggingMiddleware.cs
public class LoggingMiddleware<TState> : IMiddleware<TState>
    where TState : notnull
{
    private readonly ILogger<LoggingMiddleware<TState>> _logger;

    public LoggingMiddleware(ILogger<LoggingMiddleware<TState>> logger)
    {
        _logger = logger;
    }

    public Task OnBeforeUpdateAsync(TState currentState, string? action)
    {
        _logger.LogInformation(
            "Before update - Action: {Action}, State: {@State}",
            action ?? "Unknown",
            currentState);
        return Task.CompletedTask;
    }

    public Task OnAfterUpdateAsync(TState newState, string? action)
    {
        _logger.LogInformation(
            "After update - Action: {Action}, New State: {@State}",
            action ?? "Unknown",
            newState);
        return Task.CompletedTask;
    }
}
2

Register the Middleware

Program.cs
// Option 1: Using WithMiddleware
builder.Services.AddStore(
    new AppState(),
    (store, sp) => store.WithMiddleware(
        new LoggingMiddleware<AppState>(
            sp.GetRequiredService<ILogger<LoggingMiddleware<AppState>>>())));

// Option 2: Manual builder setup
var logger = serviceProvider.GetRequiredService<ILogger<LoggingMiddleware<AppState>>>();
var middleware = new LoggingMiddleware<AppState>(logger);

builder.Services.AddStore(
    new AppState(),
    (store, sp) => store.WithMiddleware(middleware));

Advanced Examples

Validation Middleware

Enforce business rules before state updates:

public class ValidationMiddleware<TState> : IMiddleware<TState>
    where TState : notnull
{
    public Task OnBeforeUpdateAsync(TState currentState, string? action)
    {
        if (currentState is CartState cart && cart.Total > 10000)
        {
            throw new InvalidOperationException("Cart total exceeds limit");
        }
        return Task.CompletedTask;
    }

    public Task OnAfterUpdateAsync(TState newState, string? action)
        => Task.CompletedTask;
}

Analytics Middleware

Track user actions for analytics:

public class AnalyticsMiddleware<TState> : IMiddleware<TState>
    where TState : notnull
{
    private readonly IAnalyticsService _analytics;

    public AnalyticsMiddleware(IAnalyticsService analytics)
    {
        _analytics = analytics;
    }

    public Task OnBeforeUpdateAsync(TState currentState, string? action)
        => Task.CompletedTask;

    public async Task OnAfterUpdateAsync(TState newState, string? action)
    {
        if (!string.IsNullOrEmpty(action))
        {
            await _analytics.TrackEventAsync(new AnalyticsEvent
            {
                Name = action,
                Properties = new { StateType = typeof(TState).Name }
            });
        }
    }
}

Notification Middleware

Show toast notifications on specific actions:

public class NotificationMiddleware<TState> : IMiddleware<TState>
    where TState : notnull
{
    private readonly IToastService _toast;

    public NotificationMiddleware(IToastService toast)
    {
        _toast = toast;
    }

    public Task OnBeforeUpdateAsync(TState currentState, string? action)
        => Task.CompletedTask;

    public async Task OnAfterUpdateAsync(TState newState, string? action)
    {
        if (action == "ORDER_COMPLETED")
        {
            await _toast.ShowSuccessAsync("Order placed successfully!");
        }
        else if (action?.StartsWith("ERROR_") == true)
        {
            await _toast.ShowErrorAsync("An error occurred");
        }
    }
}

Middleware Pipeline

Multiple middleware instances execute in registration order:

Chaining Multiple Middleware
builder.Services.AddStore(
    new AppState(),
    (store, sp) => store
        .WithMiddleware(new ValidationMiddleware<AppState>())
        .WithMiddleware(new LoggingMiddleware<AppState>(logger))
        .WithMiddleware(new AnalyticsMiddleware<AppState>(analytics))
        .WithPersistence(sp, "app-state")
        .WithDevTools(sp, "MyApp"));
Execution Order

OnBeforeUpdateAsync: Validation → Logging → Analytics → Persistence → DevTools

OnAfterUpdateAsync: DevTools → Persistence → Analytics → Logging → Validation

Best Practices

Keep Middleware Focused

Each middleware should have a single responsibility (logging, validation, etc.)

Don't Modify State

Middleware should observe state changes, not modify them. Use Update() in components instead.

Handle Errors Gracefully

Wrap async operations in try-catch to prevent middleware failures from breaking updates.

Use Action Names

Always provide action names when updating: Update(s => s.Method(), "ACTION_NAME")

⚠️ Avoid Heavy Computation

Keep middleware lightweight. Heavy processing should be done asynchronously or outside the update flow.

⚠️ Be Careful with Ordering

Validation should come first, persistence/DevTools last to ensure valid state is saved.