A lightweight, dependency-free library for building composable pipelines in .NET. EasyPipe makes it simple to implement the middleware/pipeline pattern with a clean, fluent API.
- ✅ No Dependencies — Core library has zero external dependencies
- ✅ No Reflection — Blazing-fast execution with minimal overhead
- ✅ Simple API — Intuitive fluent interface for defining pipelines
- ✅ .NET 9 & 10 — Targets modern .NET frameworks
- ✅ Async First — Full async/await support with cancellation tokens
- ✅ Diagnostic Hooks — Optional monitoring for step execution
Install via NuGet:
dotnet add package EasyPipeFor dependency injection integration:
dotnet add package EasyPipe.Extensions.MicrosoftDependencyInjectionpublic class RequestContext
{
public string Data { get; set; }
}
public class Response
{
public string Result { get; set; }
}Implement IPipelineStep<TContext, TResult> for each processing step:
public class ValidationStep : IPipelineStep<RequestContext, Response>
{
public async Task<Response> RunAsync(
RequestContext context,
PipeDelegate<RequestContext, Response> next,
CancellationToken ct = default)
{
// Validate input
if (string.IsNullOrEmpty(context.Data))
{
return new Response { Result = "Invalid input" };
}
// Continue to next step
return await next(context, ct);
}
}
public class ProcessingStep : IPipelineStep<RequestContext, Response>
{
public async Task<Response> RunAsync(
RequestContext context,
PipeDelegate<RequestContext, Response> next,
CancellationToken ct = default)
{
// Process the data
context.Data = context.Data.ToUpper();
return await next(context, ct);
}
}
public class ResultStep : IPipelineStep<RequestContext, Response>
{
public Task<Response> RunAsync(
RequestContext context,
PipeDelegate<RequestContext, Response> next,
CancellationToken ct = default)
{
// Terminal step - return the response
return Task.FromResult(new Response
{
Result = context.Data
});
}
}Register your pipeline in the dependency injection container:
services.AddPipeline<RequestContext, Response>(pipeline =>
{
pipeline
.AddStep<ValidationStep>()
.AddStep<ProcessingStep>()
.AddStep<ResultStep>()
});Inject and execute the pipeline:
[ApiController]
[Route("[controller]")]
public class DataController
{
private readonly IPipeline<RequestContext, Response> _pipeline;
public DataController(IPipeline<RequestContext, Response> pipeline)
{
_pipeline = pipeline;
}
[HttpPost]
public async Task<ActionResult<Response>> Process([FromBody] RequestContext request)
{
var result = await _pipeline.ExecuteAsync(request);
return Ok(result);
}
}Steps are executed in registration order. Each step receives:
- context — The current request/context object
- next — A delegate to invoke the next step in the pipeline
- ct — A cancellation token for graceful cancellation
Steps can:
- Continue — Call
await next(context, ct)to proceed to the next step - Short-circuit — Return early without calling
nextto skip remaining steps - Transform — Modify the context before passing it forward or modify the response on the way back
public class CacheStep : IPipelineStep<RequestContext, Response>
{
private readonly IMemoryCache _cache;
public CacheStep(IMemoryCache cache) => _cache = cache;
public async Task<Response> RunAsync(
RequestContext context,
PipeDelegate<RequestContext, Response> next,
CancellationToken ct = default)
{
// Check cache
if (_cache.TryGetValue(context.Data, out Response cached))
{
return cached;
}
// Call next steps
var result = await next(context, ct);
// Store in cache
_cache.Set(context.Data, result, TimeSpan.FromHours(1));
return result;
}
}When retrieving an article, check multiple storage systems (memory, Redis, database) in order. If found in a lower-priority storage, cache it in higher-priority storages.
Validate → Authorize → Log → Process → Transform → Return
Parse → Validate → Enrich → Filter → Format → Serialize
Represents a single step in the pipeline. Implement this interface to create custom processing steps.
Task<TResult> RunAsync(
TContext context,
PipeDelegate<TContext, TResult> next,
CancellationToken ct = default);The main pipeline interface used for dependency injection.
Task<TResult> ExecuteAsync(TContext context, CancellationToken ct = default);Fluent builder for configuring pipelines.
builder.AddStep<StepType>().Build();Optional interface for monitoring pipeline execution:
void OnStepStarting(Type stepType, int stepIndex);
void OnStepCompleted(Type stepType, int stepIndex, TimeSpan duration);
void OnStepFailed(Type stepType, int stepIndex, Exception exception, TimeSpan duration);
void OnPipelineCompleted(TimeSpan duration, bool success);- Keep steps focused — Each step should have a single responsibility
- Respect cancellation — Honor the cancellation token for long-running operations
- Handle exceptions gracefully — Catch and handle errors appropriately in steps
- Use dependency injection — Steps can declare dependencies via constructor injection
- Call next — Remember to await
next()unless intentionally short-circuiting - Immutable contexts — Consider making contexts immutable to avoid unexpected mutations
See the samples/EasyPipe.WebApi directory for complete working examples demonstrating:
- Basic pipeline execution with multiple steps
- Multi-step data transformation
- Cache lookup across multiple storage systems
- ASP.NET Core integration
MIT
Contributions are welcome! Please feel free to submit pull requests or open issues on GitHub.