Skip to content

Commit 3bad7fd

Browse files
committed
Improves pipeline composition and documentation
Introduces comprehensive documentation, offering guidelines for creating builders, binders, and middleware, ensuring consistency and clarity across the codebase. Refactors builder generic type arguments for enhanced clarity.
1 parent 7752ea6 commit 3bad7fd

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+617
-390
lines changed

docs/.todo.md

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1 @@
11
# todo
2-
3-
## Builder generic type arg names
4-
5-
Consider refactoring the builder generic type args to be more clear. It is a difficult naming problem
6-
because the inputs of the current builder and the inputs of the builder being created overlap. Naming of
7-
type args that make sense in both contexts has proven difficult. The compositional nature of pipelines
8-
is also a complicating factor; `TInput`, for instance, isn't the input to the current builder but is the
9-
first input to the pipeline.
10-
11-
The current argument convention is [`TInput`, `TOutput`, `TNext`] where:
12-
13-
- `TInput` is always the initial pipeline input
14-
- `TOutput` is the _current_ builder output (the next function's input)
15-
- `TNext` is the next function output
16-
17-
Should `TInput` be renamed to `TStart` or `TFirst`?
18-
Should `TNext` be renamed to make it clear that it is the next `TOutput`?

docs/advanced-patterns.md

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
---
2+
layout: default
3+
title: Advanced Patterns
4+
nav_order: 7
5+
---
6+
7+
# Advanced Pipeline Patterns
8+
9+
This page demonstrates advanced usage of the Hyperbee Pipeline library, combining extension methods, custom binders, and middleware for powerful and flexible pipeline composition.
10+
11+
## Combining Custom Middleware, Extension Methods, and a Binder
12+
13+
Suppose you want to:
14+
15+
- Add logging to every step (middleware)
16+
- Add a custom step via an extension method
17+
- Use a custom binder to control flow (e.g., retry on failure)
18+
19+
### Custom Logging Middleware
20+
21+
```csharp
22+
public static class PipelineMiddleware
23+
{
24+
public static IPipelineStartBuilder<TStart, TOutput> WithLogging<TStart, TOutput>(this IPipelineStartBuilder<TStart, TOutput> builder)
25+
{
26+
return builder.HookAsync(async (ctx, arg, next) =>
27+
{
28+
Console.WriteLine($"[LOG] Before: {arg}");
29+
var result = await next(ctx, arg);
30+
Console.WriteLine($"[LOG] After: {result}");
31+
return result;
32+
});
33+
}
34+
}
35+
```
36+
37+
### Custom Step via Extension Method
38+
39+
```csharp
40+
public static class PipelineExtensions
41+
{
42+
public static IPipelineBuilder<TStart, TOutput> WithCustomTransform<TStart, TOutput>(
43+
this IPipelineBuilder<TStart, TOutput> builder, Func<TOutput, TOutput> transform)
44+
{
45+
return builder.Pipe((ctx, arg) => transform(arg));
46+
}
47+
}
48+
```
49+
50+
### Custom Retry Binder
51+
52+
```csharp
53+
public class RetryBinder<TStart, TOutput> : Binder<TStart, TOutput>
54+
{
55+
private readonly int _maxRetries;
56+
public RetryBinder(FunctionAsync<TStart, TOutput> function, int maxRetries = 3)
57+
: base(function, null) => _maxRetries = maxRetries;
58+
59+
public FunctionAsync<TStart, TOutput> Bind(FunctionAsync<TStart, TOutput> next)
60+
{
61+
return async (context, argument) =>
62+
{
63+
int attempt = 0;
64+
while (true)
65+
{
66+
try { return await next(context, argument); }
67+
catch when (++attempt < _maxRetries) { }
68+
}
69+
};
70+
}
71+
}
72+
```
73+
74+
### Usage Example
75+
76+
```csharp
77+
var pipeline = PipelineFactory
78+
.Start<string>()
79+
.WithLogging()
80+
.Pipe((ctx, arg) => arg + " step1")
81+
.WithCustomTransform(s => s.ToUpper())
82+
.Pipe((ctx, arg) => arg + " step2")
83+
.Pipe((ctx, arg) => throw new Exception("fail"))
84+
.Pipe((ctx, arg) => new RetryBinder<string, string>(null, 3).Bind((c, a) => Task.FromResult(a)))
85+
.Build();
86+
87+
var result = await pipeline(new PipelineContext(), "input");
88+
```
89+
90+
This example demonstrates how to combine middleware, extension methods, and a custom binder for advanced scenarios.
91+
92+
---
93+
94+
For more, see [Extending Pipelines](extending.md) and [Middleware](middleware.md).

docs/command-pattern.md

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,16 @@ nav_order: 6
77
# Command Pattern
88

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

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

1818
## Example 1
19+
1920
Example of a command that takes an input and produces an output.
2021

2122
```csharp
@@ -53,6 +54,7 @@ void usage( IMyCommand command )
5354
```
5455

5556
## Example 2
57+
5658
Example of a command that takes no input and produces an output.
5759

5860
```csharp
@@ -90,6 +92,7 @@ void usage( IMyCommand command )
9092
```
9193

9294
## Example 3
95+
9396
Example of a command that takes an input and produces no output.
9497

9598
```csharp
@@ -124,4 +127,4 @@ void usage( IMyCommand command )
124127
{
125128
var result = await command.ExecuteAsync( "me" ); // returns "Hello me"
126129
}
127-
```
130+
```

docs/conventions.md

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
---
2+
layout: default
3+
title: Conventions
4+
nav_order: 3
5+
---
6+
7+
# Conventions
8+
9+
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.
10+
11+
## Generic Type Parameter Naming
12+
13+
- **TStart**: The initial input type to the pipeline. This type remains constant throughout the pipeline's lifetime.
14+
- **TOutput**: The output type of the current builder or binder. This is the type produced by the current step and consumed by the next.
15+
- **TNext**: The output type of the next function in the pipeline chain.
16+
17+
### Example
18+
19+
```csharp
20+
public class PipelineBuilder<TStart, TOutput> { /* ... */ }
21+
public interface IPipelineBuilder<TStart, TOutput> { /* ... */ }
22+
```
23+
24+
## Builder and Binder Patterns
25+
26+
- Builders and binders should be designed to maximize composability and type safety.
27+
- Prefer strongly-typed generics over `object` wherever possible.
28+
- Use clear, descriptive names for builder and binder classes to indicate their role in the pipeline.
29+
- Document the expected input and output types in XML comments.
30+
31+
## Middleware Conventions
32+
33+
### Hook Middleware
34+
35+
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:
36+
37+
```csharp
38+
public delegate Task<TOutput> MiddlewareAsync<TStart, TOutput>(IPipelineContext context, TStart argument, FunctionAsync<TStart, TOutput> next);
39+
```
40+
41+
### Wrap Middleware
42+
43+
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:
44+
45+
```csharp
46+
public delegate Task<object> MiddlewareAsync<object, object>(IPipelineContext context, object argument, FunctionAsync<object, object> next);
47+
```
48+
49+
When implementing wrap middleware:
50+
51+
- Use `object` for input and output types.
52+
- Document the expected types and perform runtime checks and casts as needed.
53+
- Only use this pattern for middleware that must be able to wrap arbitrary pipeline segments.
54+
55+
This distinction allows hook middleware to remain type-safe, while enabling wrap middleware to provide maximum flexibility.
56+
57+
- Middleware should be implemented using the `MiddlewareAsync<TStart, TOutput>` delegate:
58+
```csharp
59+
public delegate Task<TOutput> MiddlewareAsync<TStart, TOutput>(IPipelineContext context, TStart argument, FunctionAsync<TStart, TOutput> next);
60+
```
61+
62+
## Extending the Pipeline
63+
64+
- When creating custom builders or binders, follow the established naming and type parameter conventions.
65+
- Register new pipeline steps using extension methods for discoverability.
66+
- Provide XML documentation and usage examples for all public APIs.
67+
68+
## Documentation and Examples
69+
70+
- All new builders, binders, and middleware should be documented in the `docs/` directory.
71+
- Include code samples and diagrams where appropriate.
72+
73+
---
74+
75+
For more information, see the [middleware documentation](middleware.md) and the [API reference](index.md).

docs/docs.projitems

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,14 @@
1010
</PropertyGroup>
1111
<ItemGroup>
1212
<None Include="$(MSBuildThisFileDirectory)child-pipeline.md" />
13+
<None Include="$(MSBuildThisFileDirectory)conventions.md" />
1314
<None Include="$(MSBuildThisFileDirectory)dependency-injection.md" />
1415
<None Include="$(MSBuildThisFileDirectory)command-pattern.md" />
1516
<None Include="$(MSBuildThisFileDirectory)syntax.md" />
1617
<None Include="$(MSBuildThisFileDirectory)index.md" />
1718
<None Include="$(MSBuildThisFileDirectory)middleware.md" />
1819
<None Include="$(MSBuildThisFileDirectory)_config.yml" />
20+
<None Include="$(MSBuildThisFileDirectory)advanced-patterns.md" />
1921
</ItemGroup>
2022
<ItemGroup>
2123
<Content Include="$(MSBuildThisFileDirectory)_includes\nav_footer_custom.html" />

docs/extending.md

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
---
2+
layout: default
3+
title: Extending Pipelines
4+
nav_order: 6
5+
---
6+
7+
# Extending Pipelines
8+
9+
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.
10+
11+
## Extending with Extension Methods
12+
13+
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.
14+
15+
### Example: Adding a Custom Step
16+
17+
```csharp
18+
public static class PipelineExtensions
19+
{
20+
public static IPipelineBuilder<TStart, TOutput> WithCustomStep<TStart, TOutput>(
21+
this IPipelineBuilder<TStart, TOutput> builder)
22+
{
23+
return builder.Pipe((ctx, arg) => /* custom logic */);
24+
}
25+
}
26+
```
27+
28+
- Extension methods should be generic and type-safe where possible.
29+
- Provide usage examples in the documentation.
30+
31+
## When to Create a Binder
32+
33+
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.
34+
35+
### Example: Custom Binder for Random Skip
36+
37+
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.
38+
39+
```csharp
40+
public class RandomSkipBinder<TStart, TOutput> : Binder<TStart, TOutput>
41+
{
42+
private readonly Random _random = new();
43+
44+
public RandomSkipBinder(FunctionAsync<TStart, TOutput> function, Action<IPipelineContext> configure = null)
45+
: base(function, configure) { }
46+
47+
public FunctionAsync<TStart, TOutput> Bind(FunctionAsync<TStart, TOutput> next)
48+
{
49+
return async (context, argument) =>
50+
{
51+
// 50% chance to skip the next step
52+
if (_random.NextDouble() < 0.5)
53+
{
54+
// Skip the next step and just return the current result
55+
return await Pipeline(context, argument);
56+
}
57+
// Otherwise, continue as normal
58+
return await next(context, argument);
59+
};
60+
}
61+
}
62+
```
63+
64+
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.
65+
66+
## Best Practices
67+
68+
- Prefer extension methods for most customizations.
69+
- Use binders only for advanced or structural changes to pipeline flow.
70+
- Keep extension methods and binders well-documented and tested.
71+
72+
---
73+
74+
For more information, see the [conventions](conventions.md) and [middleware](middleware.md) documentation.

0 commit comments

Comments
 (0)