DEV Community

Mark Jack
Mark Jack

Posted on

b-state behaviours and processors

A clean, composable way to handle cross-cutting concerns in Blazor with BState

Image description

In the previous article, I introduced B-State, a lightweight and elegant state management library designed for Blazor. We looked at how B-State treats actions as first-class citizens and how dispatching them affects state updates predictably.

Now, let’s explore how B-State allows us to structure cross-cutting concerns like logging, exception handling, or validation using a pipeline of behaviours and processors.


🧱 The Pipeline Architecture

In B-State, actions flow through a processing pipeline that supports:

  • Behaviours
  • Preprocessors
  • Postprocessors

This architecture is inspired by the middleware pattern commonly seen in ASP.NET Core and MediatR, but tailored to the Blazor component model and optimized for state flow.

The goal is to decouple concerns such as logging, validation, or side-effects from the core business logic, contributing to a clean, maintainable architecture.

Let’s break it down.


🔁 Behaviours

Behaviours are like middleware. They wrap around the action execution and have full control over what happens before and after the action handler runs.

Here’s a logging behaviour:

class LogBehaviour : IBehaviour
{
    public async Task Run(IAction parameter, Func<IAction, Task> next)
    {
        Console.WriteLine($"{parameter.GetType().Name} started");
        await next(parameter);
        Console.WriteLine($"{parameter.GetType().Name} ended");
    }
}  
Enter fullscreen mode Exit fullscreen mode

Behaviours are global. They are registered once and wrap every action unless filtered internally.


⏮️ Preprocessors

Preprocessors run before the action handler is invoked.
Example: A global preprocessor for all actions:

class AllRequestPreprocessor<TAction> : IPreProcessor<TAction>
where TAction : IAction
{
    public Task Run(IAction parameter)
    {
        Console.WriteLine("AllRequestPreprocessor");
        return Task.CompletedTask;
    }
}
Enter fullscreen mode Exit fullscreen mode

You can also conditionally execute preprocessors based on action interfaces. For example, if you tag long-running operations with ILongAction:

// Marker interface
public interface ILongAction { }
Enter fullscreen mode Exit fullscreen mode
class ALongActionPreProcessor<TRequest> : IPreProcessor<TRequest>
where TRequest : ILongAction
{
    public Task Run(IAction parameter)
    {
        Console.WriteLine("This is a long action... please wait!");
        return Task.CompletedTask;
    }
}
Enter fullscreen mode Exit fullscreen mode

This preprocessor is activated only when IAction implements ILongAction. This feature provides great flexibility when building the pipeline to manage your requests.

⏭️ Postprocessors

Postprocessors work exactly like preprocessors, but are executed after the action handler runs. Use them for tasks like:

  • Cleanup
  • Logging results
  • Updating external systems

To implement one:

class MyPostProcessor<TAction> : IPostProcessor<TAction>
where TAction : IAction
{
    public Task Run(IAction parameter)
    {
        Console.WriteLine("Action completed.");
        return Task.CompletedTask;
    }
}
Enter fullscreen mode Exit fullscreen mode

🧩 Putting It Together

builder.Services.AddBState(configuration =>
{
    configuration.RegisterFrom(Assembly.GetExecutingAssembly());

    configuration.AddBehaviour<ExceptionBehaviour>();
    configuration.AddBehaviour<LogBehaviour>();

    configuration.AddOpenRequestPreProcessor(typeof(AllRequestPreprocessor<>));
    configuration.AddOpenRequestPreProcessor(typeof(ALongActionPreProcessor<>));
    // configuration.AddOpenRequestPostProcessor(typeof(MyPostProcessor<>));
});
Enter fullscreen mode Exit fullscreen mode

With behaviours, preprocessors, and postprocessors, B-State introduces a powerful yet elegantly simple pipeline model for handling cross-cutting concerns in Blazor applications. Instead of embedding logging, error handling, loading indicators, or validation logic directly into your components or action handlers, you delegate them to modular units that wrap or surround the core logic. This keeps your actions focused on business rules, while the supporting concerns remain isolated, reusable, and composable.

The key principle here is composability. Each behaviour or processor is independently testable, context-agnostic, and easily replaceable, enabling teams to iterate faster without fear of regressions or side effects. Whether you’re building large enterprise applications or small Blazor components, this structure brings clarity, separation of concerns, and scalability.

B-State’s pipeline architecture provides more than just state management — it offers a clean, extensible framework to handle the complexities that naturally emerge as your application grows. By leveraging behaviours and processors, you gain precise control over how actions are prepared, executed, and handled, while keeping your application logic lean and expressive.


For a repo example of this tutorial see HERE, for a more complete example HERE
If you found this article helpful, leave a ⭐️ on the library’s repository on GitHub and follow me on GitHub, Twitter and Bluesky for more content.

Thanks for reading!

Top comments (0)