Knowledge Base
Software
2025-08-09
10 min

PipeR: A Lightweight .NET Pipeline Library for Clean Request Handling

A composable alternative to MediatR — build clean, performant request pipelines in .NET using strongly typed middleware and fluent configuration.

.NET
CQRS
Middleware
Clean Architecture
Performance
MediatR Alternative

PipeR is a purpose-built middleware-style pipeline library for .NET applications, designed to streamline request/response handling within clean architectures. Inspired by the Mediator pattern (à la MediatR) but built with clarity, performance, and modularity in mind, PipeR empowers developers to structure business logic as a series of composable steps.

🔍 Why Create PipeR?

PipeR was born from a combination of architectural need, performance considerations, and a shift in the open-source ecosystem surrounding .NET.

Like many .NET developers, I relied heavily on MediatR for years as the go-to library for decoupling request handling. However, the announcement by its creator Jimmy Bogard that MediatR would be transitioning to a commercial license model sparked reflection — not just about licensing, but about control, transparency, and long-term project autonomy.

This commercialization trend, also affecting libraries like AutoMapper, is well covered by Milan Jovanovic in his thoughtful commentary: “Open Source in .NET is Changing”.

At the same time, inspiration struck from the community. A video by Mohamad Dbouk titled “Build Your Own Mediator in .NET” laid out a minimalist but clear implementation of the Mediator pattern. This helped crystallize my thinking: I didn’t need to fork MediatR or wait for community reactions — I could build my own approach, one tailored to the specific patterns and performance characteristics I value in my production services.

Thus, PipeR was envisioned — not as a clone, but as a composable, high-performance middleware engine that emphasizes:

  • Explicit flow control through fluent configuration
  • Strong typing for both requests and middleware
  • Lightweight footprint with zero reflection at runtime
  • Plug-and-play middleware and validation support
  • Production resilience without third-party dependency entanglement

PipeR was born out of the desire for a more controlled, pluggable, and high-performance solution — tailored for backends where performance and traceability matter.

🧱 Core Concepts

  • Request Types – Define the request + response types (e.g., LoginUserRequest : IRequest<LoginResponse>)
  • Handlers – Pure handlers containing business logic
  • Middleware – Pre/post-processing components injected into the pipeline (e.g., logging, metrics, validation)
  • PipelineBuilder – Fluent API to register steps and compose the request execution flow

📦 Basic Usage - PipelineBuilder

var pipeline = new PipelineBuilder()
    .Use<LoggingMiddleware>()
    .Use<AuthorizationMiddleware>()
    .UseHandler<LoginRequestHandler>()
    .Build();

var result = await pipeline.ExecuteAsync(new LoginUserRequest { ... });

Each middleware gets a chance to inspect, short-circuit, or transform the request/response. Everything is strongly typed, and you remain in full control of registration order and dependency resolution.

📚 Request/Response Separation (CQRS-Light)

While PipeR focuses heavily on composable middleware pipelines, it also promotes a clean and consistent request/response structure. Inspired by the CQRS pattern, this approach brings clarity and separation of concerns without the complexity of full-blown CQRS implementations.

Here's how simple a controller becomes:

[HttpGet]
public async Task<ActionResult<GetAllBooksResponse>>
    GetBooks([FromQuery] GetAllBooksQuery query) =>
    await this.HandleRequest(query);

And with the help of a base controller abstraction, you inject your IPiper dependency once and unify request routing:

public class BookControllerBase : ControllerBase
{
    protected readonly IPiper _piper;

    public BookControllerBase(IPiper piper)
    {
        _piper = piper;
    }

    protected async Task<ActionResult> HandleRequest<TResponse>(IRequest<TResponse> request)
    {
        var userIdClaim = User.FindFirst("UserId");
        if (userIdClaim != null && request is BaseRequest baseRequest)
        {
            baseRequest.UserId = Guid.Parse(userIdClaim.Value);
            return Ok(await _piper.Send(request));
        }

        return Forbid();
    }
}

📝 Requests and Responses

public class GetAllBooksRequest : BaseRequest
{
    public int PageNumber { get; set; } = 1;
    public int PageSize { get; set; } = 20;
    public string? SearchTerm { get; set; }
}

public class GetAllBooksResponse : BaseResponse
{
    public PagedResult<BookDto> Books { get; set; } = new();
}

🔀 Query + Validation Layer

public class GetAllBooksQuery : GetAllBooksRequest, IRequest<GetAllBooksResponse> {}

public class GetAllBooksValidator : AbstractValidator<GetAllBooksQuery>
{
    public GetAllBooksValidator()
    {
        RuleFor(x => x.ApiKey)
            .NotEmpty().WithMessage("A valid API Key must be provided!");
        RuleFor(x => x.UserId)
            .NotNull().WithMessage("User Id must be supplied!");
    }
}

⚙️ Abstract Request Handler

Your handlers can inherit from a base class that encapsulates common patterns, enforcing consistency and reducing duplication:

public abstract class RequestHandlerBase<TRequest, TResponse, TValidator> 
    : IRequestHandler<TRequest, TResponse>
    where TRequest : BaseRequest, IRequest<TResponse>
    where TResponse : BaseResponse, new()
    where TValidator : AbstractValidator<TRequest>, new()
{
    // Abstract Handle logic with optional validation
}

This architecture ensures that all requests are routed, validated, and handled consistently — reducing boilerplate and centralizing behavior for cross-cutting concerns.

📊 Advantages Over MediatR

  • 🚀 No reflection or dynamic resolution overhead
  • 🧩 Middleware chaining using pure DI or fluent configuration
  • 💬 Transparent request flow, easier debugging
  • 🛡️ Cleaner separation of concerns in complex apps

💡 When to Use PipeR

PipeR excels in systems that:

  • Need structured pipelines (like CQRS or request-response)
  • Emphasize testability, modularity, and observability
  • Run in high-performance or resource-constrained environments (e.g., containers, microservices)
  • Desire a MediatR alternative without the baggage

🧪 Example Scenario: Validating and Handling a Login

// Middleware
public class ValidationMiddleware<TRequest, TResponse> : IPipelineMiddleware<TRequest, TResponse>
{
    public async Task<TResponse> InvokeAsync(
        TRequest request, 
        Func<TRequest, Task<TResponse>> next)
    {
        Validate(request); // throw on failure
        return await next(request);
    }
}

// Handler
public class LoginHandler : IRequestHandler<LoginRequest, LoginResponse>
{
    public async Task<LoginResponse> Handle(LoginRequest request)
    {
        return new LoginResponse { Token = "abc123" };
    }
}

🌐 GitHub

The PipeR project will soon be available on GitHub, alongside full documentation and integration examples.

📦 NuGet Plans

The library will be packaged into two NuGet packages:

  • PipeR.Core – Core pipeline interfaces and engine
  • PipeR.AspNetCore – an extension on top of Core for use in ASP.NET Core projects

📚 Coming Soon

  • 🔌 MediatR compatibility bridge
  • 🎯 Benchmark comparisons
  • 📈 OpenTelemetry middleware
  • 🧪 Unit testing harnesses for pipelines and middleware

PipeR is your fast track to clean, composable, middleware-first request handling in .NET.

Filed under: .NET, CQRS, Middleware, Clean Architecture, Performance, MediatR Alternative

Last updated: 2025-08-09