Skip to content

Improves pipeline composition and documentation #63

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Jul 16, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
<!-- Solution version numbers -->
<PropertyGroup>
<MajorVersion>2</MajorVersion>
<MinorVersion>0</MinorVersion>
<PatchVersion>3</PatchVersion>
<MinorVersion>1</MinorVersion>
<PatchVersion>0</PatchVersion>
</PropertyGroup>
<!-- Disable automatic package publishing -->
<PropertyGroup>
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ Some key features are:
Pipelines provide a structured approach to managing complex processes, promoting [SOLID](https://en.wikipedia.org/wiki/SOLID)
principles, including Inversion of Control (IoC) and Separation of Concerns (SoC). They enable composability, making it easier
to build, test, and maintain your code. By extending the benefits of middleware and request-response pipelines throughout your
application, you achieve greater modularity, scalability, and flexibility. This is especially critical in domains such as
healthcare, compliance auditing, identity and roles, and high-security environments where clear boundaries and responsibilities
application, you achieve greater modularity, scalability, and flexibility. This is particularly important in domains that demand
compliance, auditing, strong identity and role management, or high-security standards—where clear boundaries and responsibilities
are essential. Hyperbee.Pipeline ensures that the advantages of pipelines and middleware are not abandoned at the controller
implementation, addressing a common gap in many frameworks. By using a functional approach, Hyperbee.Pipeline ensures that your
pipelines are not only robust and maintainable but also highly adaptable to changing requirements.
Expand Down
17 changes: 0 additions & 17 deletions docs/.todo.md
Original file line number Diff line number Diff line change
@@ -1,18 +1 @@
# todo

## Builder generic type arg names

Consider refactoring the builder generic type args to be more clear. It is a difficult naming problem
because the inputs of the current builder and the inputs of the builder being created overlap. Naming of
type args that make sense in both contexts has proven difficult. The compositional nature of pipelines
is also a complicating factor; `TInput`, for instance, isn't the input to the current builder but is the
first input to the pipeline.

The current argument convention is [`TInput`, `TOutput`, `TNext`] where:

- `TInput` is always the initial pipeline input
- `TOutput` is the _current_ builder output (the next function's input)
- `TNext` is the next function output

Should `TInput` be renamed to `TStart` or `TFirst`?
Should `TNext` be renamed to make it clear that it is the next `TOutput`?
94 changes: 94 additions & 0 deletions docs/advanced-patterns.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
---
layout: default
title: Advanced Patterns
nav_order: 7
---

# Advanced Pipeline Patterns

This page demonstrates advanced usage of the Hyperbee Pipeline library, combining extension methods, custom binders, and middleware for powerful and flexible pipeline composition.

## Combining Custom Middleware, Extension Methods, and a Binder

Suppose you want to:

- Add logging to every step (middleware)
- Add a custom step via an extension method
- Use a custom binder to control flow (e.g., retry on failure)

### Custom Logging Middleware

```csharp
public static class PipelineMiddleware
{
public static IPipelineStartBuilder<TStart, TOutput> WithLogging<TStart, TOutput>(this IPipelineStartBuilder<TStart, TOutput> builder)
{
return builder.HookAsync(async (ctx, arg, next) =>
{
Console.WriteLine($"[LOG] Before: {arg}");
var result = await next(ctx, arg);
Console.WriteLine($"[LOG] After: {result}");
return result;
});
}
}
```

### Custom Step via Extension Method

```csharp
public static class PipelineExtensions
{
public static IPipelineBuilder<TStart, TOutput> WithCustomTransform<TStart, TOutput>(
this IPipelineBuilder<TStart, TOutput> builder, Func<TOutput, TOutput> transform)
{
return builder.Pipe((ctx, arg) => transform(arg));
}
}
```

### Custom Retry Binder

```csharp
public class RetryBinder<TStart, TOutput> : Binder<TStart, TOutput>
{
private readonly int _maxRetries;
public RetryBinder(FunctionAsync<TStart, TOutput> function, int maxRetries = 3)
: base(function, null) => _maxRetries = maxRetries;

public FunctionAsync<TStart, TOutput> Bind(FunctionAsync<TStart, TOutput> next)
{
return async (context, argument) =>
{
int attempt = 0;
while (true)
{
try { return await next(context, argument); }
catch when (++attempt < _maxRetries) { }
}
};
}
}
```

### Usage Example

```csharp
var pipeline = PipelineFactory
.Start<string>()
.WithLogging()
.Pipe((ctx, arg) => arg + " step1")
.WithCustomTransform(s => s.ToUpper())
.Pipe((ctx, arg) => arg + " step2")
.Pipe((ctx, arg) => throw new Exception("fail"))
.Pipe((ctx, arg) => new RetryBinder<string, string>(null, 3).Bind((c, a) => Task.FromResult(a)))
.Build();

var result = await pipeline(new PipelineContext(), "input");
```

This example demonstrates how to combine middleware, extension methods, and a custom binder for advanced scenarios.

---

For more, see [Extending Pipelines](extending.md) and [Middleware](middleware.md).
17 changes: 10 additions & 7 deletions docs/command-pattern.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,16 @@ nav_order: 6
# Command Pattern

`ICommand*` interfaces and `Command*` base classes provide a lightweight pattern for constructing injectable commands built
around pipelines and middleware.
around pipelines and middleware.

| Interface | Class | Description
| -------------------------------------- | ------------------------------------- | ---------------------------------------------------
| ICommandFunction&lt;TInput,TOutput&gt; | CommandFunction&lt;TInput,TOutput&gt; | A command that takes an input and returns an output
| ICommandFunction&lt;TOutput&gt; | CommandFunction&lt;TOutput&gt; | A command that takes no input and returns an output
| ICommandProcedure&lt;TInput&gt; | CommandProcedure&lt;TInput&gt; | A command that takes an input and returns void
| Interface | Class | Description |
| -------------------------------------- | ------------------------------------- | --------------------------------------------------- |
| ICommandFunction&lt;TStart,TOutput&gt; | CommandFunction&lt;TStart,TOutput&gt; | A command that takes an input and returns an output |
| ICommandFunction&lt;TOutput&gt; | CommandFunction&lt;TOutput&gt; | A command that takes no input and returns an output |
| ICommandProcedure&lt;TStart&gt; | CommandProcedure&lt;TStart&gt; | A command that takes an input and returns void |

## Example 1

Example of a command that takes an input and produces an output.

```csharp
Expand Down Expand Up @@ -53,6 +54,7 @@ void usage( IMyCommand command )
```

## Example 2

Example of a command that takes no input and produces an output.

```csharp
Expand Down Expand Up @@ -90,6 +92,7 @@ void usage( IMyCommand command )
```

## Example 3

Example of a command that takes an input and produces no output.

```csharp
Expand Down Expand Up @@ -124,4 +127,4 @@ void usage( IMyCommand command )
{
var result = await command.ExecuteAsync( "me" ); // returns "Hello me"
}
```
```
75 changes: 75 additions & 0 deletions docs/conventions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
---
layout: default
title: Conventions
nav_order: 3
---

# Conventions

This document describes the conventions for creating builders, binders, and middleware in the Hyperbee Pipeline library. Adhering to these conventions ensures consistency, maintainability, and clarity across the codebase and for all contributors.

## Generic Type Parameter Naming

- **TStart**: The initial input type to the pipeline. This type remains constant throughout the pipeline's lifetime.
- **TOutput**: The output type of the current builder or binder. This is the type produced by the current step and consumed by the next.
- **TNext**: The output type of the next function in the pipeline chain.

### Example

```csharp
public class PipelineBuilder<TStart, TOutput> { /* ... */ }
public interface IPipelineBuilder<TStart, TOutput> { /* ... */ }
```

## Builder and Binder Patterns

- Builders and binders should be designed to maximize composability and type safety.
- Prefer strongly-typed generics over `object` wherever possible.
- Use clear, descriptive names for builder and binder classes to indicate their role in the pipeline.
- Document the expected input and output types in XML comments.

## Middleware Conventions

### Hook Middleware

Hook middleware is always generic and type-safe. It is inserted at known points in the pipeline where the input and output types are known. Always use generic signatures for hook middleware:

```csharp
public delegate Task<TOutput> MiddlewareAsync<TStart, TOutput>(IPipelineContext context, TStart argument, FunctionAsync<TStart, TOutput> next);
```

### Wrap Middleware

Wrap middleware must be able to wrap any pipeline segment, regardless of its input and output types. To enable this, wrap middleware uses `object` for its input and output types. This is a necessary compromise in C# to allow full compositionality:

```csharp
public delegate Task<object> MiddlewareAsync<object, object>(IPipelineContext context, object argument, FunctionAsync<object, object> next);
```

When implementing wrap middleware:

- Use `object` for input and output types.
- Document the expected types and perform runtime checks and casts as needed.
- Only use this pattern for middleware that must be able to wrap arbitrary pipeline segments.

This distinction allows hook middleware to remain type-safe, while enabling wrap middleware to provide maximum flexibility.

- Middleware should be implemented using the `MiddlewareAsync<TStart, TOutput>` delegate:
```csharp
public delegate Task<TOutput> MiddlewareAsync<TStart, TOutput>(IPipelineContext context, TStart argument, FunctionAsync<TStart, TOutput> next);
```

## Extending the Pipeline

- When creating custom builders or binders, follow the established naming and type parameter conventions.
- Register new pipeline steps using extension methods for discoverability.
- Provide XML documentation and usage examples for all public APIs.

## Documentation and Examples

- All new builders, binders, and middleware should be documented in the `docs/` directory.
- Include code samples and diagrams where appropriate.

---

For more information, see the [middleware documentation](middleware.md) and the [API reference](index.md).
2 changes: 2 additions & 0 deletions docs/docs.projitems
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,14 @@
</PropertyGroup>
<ItemGroup>
<None Include="$(MSBuildThisFileDirectory)child-pipeline.md" />
<None Include="$(MSBuildThisFileDirectory)conventions.md" />
<None Include="$(MSBuildThisFileDirectory)dependency-injection.md" />
<None Include="$(MSBuildThisFileDirectory)command-pattern.md" />
<None Include="$(MSBuildThisFileDirectory)syntax.md" />
<None Include="$(MSBuildThisFileDirectory)index.md" />
<None Include="$(MSBuildThisFileDirectory)middleware.md" />
<None Include="$(MSBuildThisFileDirectory)_config.yml" />
<None Include="$(MSBuildThisFileDirectory)advanced-patterns.md" />
</ItemGroup>
<ItemGroup>
<Content Include="$(MSBuildThisFileDirectory)_includes\nav_footer_custom.html" />
Expand Down
74 changes: 74 additions & 0 deletions docs/extending.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
---
layout: default
title: Extending Pipelines
nav_order: 6
---

# Extending Pipelines

The preferred way to extend pipeline functionality is by writing extension methods. This approach is simple, type-safe, and leverages C#'s strengths. Only create a new binder if you need to introduce a fundamentally new control flow or block structure that cannot be expressed with existing builders and extension methods.

## Extending with Extension Methods

Extension methods allow you to add new pipeline steps, middleware, or behaviors without modifying the core pipeline code. Place extension methods in well-named static classes (e.g., `PipelineExtensions`, `PipelineMiddleware`) for discoverability.

### Example: Adding a Custom Step

```csharp
public static class PipelineExtensions
{
public static IPipelineBuilder<TStart, TOutput> WithCustomStep<TStart, TOutput>(
this IPipelineBuilder<TStart, TOutput> builder)
{
return builder.Pipe((ctx, arg) => /* custom logic */);
}
}
```

- Extension methods should be generic and type-safe where possible.
- Provide usage examples in the documentation.

## When to Create a Binder

Most customizations can be achieved with extension methods. However, if you need to introduce a new control flow (e.g., conditional, loop, parallel execution) or a new block structure, you may need to implement a custom binder.

### Example: Custom Binder for Random Skip

Suppose you want to add a pipeline step that randomly skips the next step in the pipeline, just for demonstration. This kind of execution control cannot be done with a simple extension method because it requires direct control over the pipeline's flow.

```csharp
public class RandomSkipBinder<TStart, TOutput> : Binder<TStart, TOutput>
{
private readonly Random _random = new();

public RandomSkipBinder(FunctionAsync<TStart, TOutput> function, Action<IPipelineContext> configure = null)
: base(function, configure) { }

public FunctionAsync<TStart, TOutput> Bind(FunctionAsync<TStart, TOutput> next)
{
return async (context, argument) =>
{
// 50% chance to skip the next step
if (_random.NextDouble() < 0.5)
{
// Skip the next step and just return the current result
return await Pipeline(context, argument);
}
// Otherwise, continue as normal
return await next(context, argument);
};
}
}
```

You would then integrate this binder into your pipeline using a builder or extension method. This example is intentionally whimsical, but it demonstrates how a custom binder can control execution flow in ways that extension methods cannot.

## Best Practices

- Prefer extension methods for most customizations.
- Use binders only for advanced or structural changes to pipeline flow.
- Keep extension methods and binders well-documented and tested.

---

For more information, see the [conventions](conventions.md) and [middleware](middleware.md) documentation.
Loading