diff --git a/README.md b/README.md index c07461c..9f4c09b 100644 --- a/README.md +++ b/README.md @@ -1,191 +1,133 @@ -# Hyperbee.Pipeline +# Hyperbee Pipeline -The `Hyperbee.Pipeline` library is a sophisticated tool for constructing asynchronous fluent pipelines in .NET. A pipeline, in this context, refers to a sequence of data processing elements arranged in series, where the output of one element serves as the input for the subsequent element. +`Hyperbee.Pipeline` allows you to construct asynchronous fluent pipelines in .NET. A pipeline, in this context, refers to a +sequence of data processing elements arranged in series, where the output of one element serves as the input for the subsequent +element. -A distinguishing feature of the `Hyperbee.Pipeline` library, setting it apart from other pipeline implementations, is its inherent support for **middleware** and **dependency injection**. Middleware introduces a higher degree of flexibility and control over the data flow, enabling developers to manipulate data as it traverses through the pipeline. This can be leveraged to implement a variety of functionalities such as eventing, caching, logging, and more, thereby enhancing the customizability of the code. +Hyperbee pipelines are composable, reusable, and easy to test. They are designed to be used in a variety of scenarios, such +as data processing, message handling, and workflow automation. -Furthermore, the support for dependency injection facilitates efficient management of dependencies within the pipeline. This leads to code that is more maintainable and testable, thereby improving the overall quality of the software. +Some key features are: - -## Features * Middleware - * Pipelines come with the ability to enhance processing with custom middleware. -* Hook - * the `Hook` and `HookAsync` method allows you to add a hook that is called for every statement in the pipeline. -* Wrap - * The `Wrap` and `WrapAsync` method allows you to wrap a part of the pipeline. -* Dependency Injection - * Sometimes Pipelines and Pipeline middleware need access to specific container services. -* **Advanced features** - * The `PipelineFactory` library provides a variety of helper methods that allow you to customize the behavior of your pipelines. - * Reduce - * The `Reduce` and `ReduceAsync` methods allow you to reduce a sequence of elements to a single value. - * WaitAll - * The `WaitAll` method allows you to wait for all pipelines to complete before continuing. - * PipeIf - * The `PipeIf` method allows you to conditionally add a step to the pipeline. - * ForEach and ForEachAsync - *The `ForEach` and `ForEachAsync` methods allow you to apply a pipeline to each element in a sequence. - * Call and CallAsync - * The `Call` and `CallAsync` methods allow you to add a procedure to the pipeline. - * Chaining Child Pipelines - * The `PipelineFactory` library allows you to chain pipelines together. - - -## Example - -```csharp -// Takes a string and returns a number -var question = PipelineFactory - .Start() - .PipeIf((ctx, arg) => arg == "Adams", builder => builder - .Pipe((ctx, arg) => 42) - .Cancel() - ) - .Pipe((ctx, arg) => 0) - .Build(); - -var answer1 = await question(new PipelineContext(), "Adams"); -Assert.AreEqual(42, answer1); - -var answer2 = await question(new PipelineContext(), "Smith"); -Assert.AreEqual(0, answer2); -``` +* Hooks +* Wraps +* Conditional flows +* Loops +* Parallel processing +* Dependency injection +* Early returns and cancellation +* Child pipelines +## Why Use Pipelines -## Example Hook +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 +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. -The `Hook` and `HookAsync` methods allow you to add a hook that is called for every statement in the pipeline. This hook takes the current context, the current argument, and a delegate to the next part of the pipeline. It can manipulate the argument before and after calling the next part of the pipeline. -Here's an example of how to use `HookAsync`: +## Getting Started -```csharp -var command = PipelineFactory - .Start() - .HookAsync( async ( ctx, arg, next ) => await next( ctx, arg + "{" ) + "}" ) - .Pipe( ( ctx, arg ) => arg + "1" ) - .Pipe( ( ctx, arg ) => arg + "2" ) - .Build(); +To get started with Hyperbee.Json, refer to the [documentation](https://stillpoint-software.github.io/hyperbee.pipeline) for +detailed instructions and examples. -var result = await command( new PipelineContext() ); +Install via NuGet: -Assert.AreEqual( "{1}{2}", result ); +```bash +dotnet add package Hyperbee.Pipeline ``` -## Example Wrap +## Building and Executing Pipelines -```csharp -var command = PipelineFactory - .Start() - .Pipe( ( ctx, arg ) => arg + "1" ) - .Pipe( ( ctx, arg ) => arg + "2" ) - .WrapAsync( async ( ctx, arg, next ) => await next( ctx, arg + "{" ) + "}" ) - .Pipe( ( ctx, arg ) => arg + "3" ) - .Build(); +Pipelines are built using `PipelineFactory`. Once built, a pipeline is just an async function that takes a `PipelineContext` and +an optional input value as parameters, and returns a result. -var result = await command( new PipelineContext() ); +```csharp + var command = PipelineFactory + .Start() + .Pipe( ( ctx, arg ) => $"hello {arg}" ) + .Build(); -Assert.AreEqual( "{12}3", result ); + var result = await command( new PipelineContext(), "pipeline" ); + Assert.AreEqual( "hello pipeline", result ); ``` -## Example ForEach +## Dependency Injection -```csharp -var count = 0; +Sometimes Pipelines and Pipeline middleware need access to specific container services. This can be +accomplished by registering services with the `PipelineContextFactory`. This can be done through +DI configuration, or manually through the `PipelineContextFactoryProvider` if you are not using DI. -var command = PipelineFactory - .Start() - .Pipe( ( ctx, arg ) => arg.Split( ' ' ) ) - .ForEach( builder => builder - .Pipe( ( ctx, arg ) => count += 10 ) - ) - .Pipe( ( ctx, arg ) => count += 5 ) - .Build(); +Pipelines manage dependencies with a specialized container. This allows the implementor to control +the services that are exposed through the pipeline. If you want to expose all application +services then you can call `AddPipeline` and pass `includeAllServices: true`. -await command( new PipelineContext(), "e f" ); +Register pipelines with DI and provide Pipeline dependencies using the application container. -Assert.AreEqual( count, 25 ); +```csharp +services.AddPipeline( includeAllServices: true ); ``` -## Example Call +Register Pipelines with DI and provide Pipeline dependencies using a specialized container. ```csharp -var callResult = string.Empty; - -var command = PipelineFactory - .Start() - .Pipe( ( ctx, arg ) => arg + "1" ) - .Pipe( ( ctx, arg ) => arg + "2" ) - .Call( builder => builder - .Call( ( ctx, arg ) => callResult = arg + "3" ) - .Pipe( ( ctx, arg ) => arg + "9" ) - ) - .Pipe( ( ctx, arg ) => arg + "4" ) - .Build(); +services.AddPipeline( (factoryServices, rootProvider) => +{ + factoryServices.AddTransient() + factoryServices.ProxyService( rootProvider ); // pull from root container +} ); +``` -var result = await command( new PipelineContext() ); +## Pipeline of Pipelines -Assert.AreEqual( "124", result ); -Assert.AreEqual( "123", callResult ); -``` +The `PipelineFactory` library allows you to use pipelines together. Since pipelines are just functions, they can be used +as input to other pipelines. This allows you to create complex data processing flows by reusing and chaining together +multiple pipelines. -## Example Chaining Child Pipelines +Here's an example of how to use pipelines together: ```csharp -var command2 = PipelineFactory +var pipeline2 = PipelineFactory .Start() .Pipe( ( ctx, arg ) => $"{arg} again!" ) .Build(); -var command1 = PipelineFactory +var pipeline1 = PipelineFactory .Start() .Pipe( ( ctx, arg ) => $"hello {arg}" ) - .PipeAsync( command2 ) + .PipeAsync( pipeline2 ) .Build(); -var result = await command1( new PipelineContext(), "pipeline" ); +var result = await pipeline1( new PipelineContext(), "you" ); -Assert.AreEqual( "hello pipeline again!", result ); +Assert.AreEqual( "hello you again!", result ); ``` -## Additional Documentation -Classes for building composable async pipelines supporting: - - * [Middleware](https://github.com/Stillpoint-Software/Hyperbee.Pipeline/blob/main/docs/middleware.md) - * [Conditional flow](https://github.com/Stillpoint-Software/Hyperbee.Pipeline/blob/main/docs/execution.md) - * [Dependency Injection](https://github.com/Stillpoint-Software/Hyperbee.Pipeline/blob/main/docs/dependencyInjection.md) - * Value projections - * Early returns - * Child pipelines - - -# Build Requirements - -* To build and run this project, **.NET 8 SDK** is required. -* Ensure your development tools are compatible with .NET 8. - -## Building the Project - -* With .NET 8 SDK installed, you can build the project using the standard `dotnet build` command. - -## Running Tests - -* Run tests using the `dotnet test` command as usual. +## Conditional Flow and Advanced Features -# Status +The `PipelineFactory` library provides a variety of builders that allow you to customize the behavior of your pipelines. +These methods provide powerful functionality for manipulating data as it passes through the pipeline. -| Branch | Action | -|------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `develop` | [![Build status](https://github.com/Stillpoint-Software/Hyperbee.Pipeline/actions/workflows/publish.yml/badge.svg?branch=develop)](https://github.com/Stillpoint-Software/Hyperbee.Pipeline/actions/workflows/publish.yml) | -| `main` | [![Build status](https://github.com/Stillpoint-Software/Hyperbee.Pipeline/actions/workflows/publish.yml/badge.svg)](https://github.com/Stillpoint-Software/Hyperbee.Pipeline/actions/workflows/publish.yml) | +- Functions +- Procedures +- Conditional Flow +- Iterators +- Reduce +- Parallel execution +## Credits +Hyperbee.Pipeline is built upon the great work of several open-source projects. Special thanks to: -# Benchmarks - See [Benchmarks](https://github.com/Stillpoint-Software/Hyperbee.Pipeline/test/Hyperbee.Pipeline.Benchmark/benchmark/results/Hyperbee.Pipeline.Benchmark.PipelineBenchmarks-report-github.md) +- [Just The Docs](https://github.com/just-the-docs/just-the-docs) for the documentation theme. -# Help - See our list of items [Todo](https://github.com/Stillpoint-Software/Hyperbee.Pipeline/blob/main/docs/todo.md) +## Contributing - [![Hyperbee.Pipeline](https://github.com/Stillpoint-Software/Hyperbee.Pipeline/blob/main/assets/hyperbee.svg?raw=true)](https://github.com/Stillpoint-Software/Hyperbee.Pipeline) +We welcome contributions! Please see our [Contributing Guide](https://github.com/Stillpoint-Software/.github/blob/main/.github/CONTRIBUTING.md) +for more details. diff --git a/docs/command-pattern.md b/docs/command-pattern.md index 9bc4d96..290b62a 100644 --- a/docs/command-pattern.md +++ b/docs/command-pattern.md @@ -19,109 +19,109 @@ nav_order: 6 Example of a command that takes an input and produces an output. ```csharp - public interface IMyCommand : ICommandFunction +public interface IMyCommand : ICommandFunction +{ +} + +public class MyCommand : CommandFunction, IMyCommand +{ + public MyCommand( ILogger logger ) + : base( logger) { } - public class MyCommand : CommandFunction, IMyCommand + protected override FunctionAsync PipelineFactory() { - public MyCommand( ILogger logger ) - : base( logger) - { - } - - protected override FunctionAsync PipelineFactory() - { - return PipelineFactory - .Start() - .WithLogging() - .Pipe( GetString ) - .Build(); - } - - private async Task GetString( IPipelineContext context, Guid id ) - { - return id.ToString(); - } + return PipelineFactory + .Start() + .WithLogging() + .Pipe( GetString ) + .Build(); } - // usage - void usage( IMyCommand command ) + private async Task GetString( IPipelineContext context, Guid id ) { - var result = await command.ExecuteAsync( Guid.Create() ); // takes a Guid, returns a string + return id.ToString(); } +} + +// usage +void usage( IMyCommand command ) +{ + var result = await command.ExecuteAsync( Guid.Create() ); // takes a Guid, returns a string +} ``` ## Example 2 Example of a command that takes no input and produces an output. ```csharp - public interface IMyCommand : ICommandFunction +public interface IMyCommand : ICommandFunction +{ +} + +public class MyCommand : CommandFunction, IMyCommand +{ + public MyCommand( ILogger logger ) + : base( logger) { } - public class MyCommand : CommandFunction, IMyCommand + protected override FunctionAsync CreatePipeline() { - public MyCommand( ILogger logger ) - : base( logger) - { - } - - protected override FunctionAsync CreatePipeline() - { - return PipelineFactory - .Start() - .WithLogging() - .PipeAsync( GetString ) - .Build(); - } - - private String GetString( IPipelineContext context, Arg.Empty _ ) - { - return "Hello"; - } + return PipelineFactory + .Start() + .WithLogging() + .PipeAsync( GetString ) + .Build(); } - // usage - void usage( IMyCommand command ) + private String GetString( IPipelineContext context, Arg.Empty _ ) { - var result = await command.ExecuteAsync(); // returns "Hello" + return "Hello"; } +} + +// usage +void usage( IMyCommand command ) +{ + var result = await command.ExecuteAsync(); // returns "Hello" +} ``` ## Example 3 Example of a command that takes an input and produces no output. ```csharp - public interface IMyCommand : ICommandProcedure +public interface IMyCommand : ICommandProcedure +{ +} + +public class MyCommand : CommandProcedure, IMyCommand +{ + public GetCommand( ILogger logger ) + : base( logger) { } - public class MyCommand : CommandProcedure, IMyCommand + protected override ProcedureAsync CreatePipeline() { - public GetCommand( ILogger logger ) - : base( logger) - { - } - - protected override ProcedureAsync CreatePipeline() - { - return PipelineFactory - .Start() - .WithLogging() - .PipeAsync( ExecSomeAction ) - .BuildAsProcedure(); - } - - private String ExecSomeAction( IPipelineContext context, String who ) - { - return $"Hello {who}"; - } + return PipelineFactory + .Start() + .WithLogging() + .PipeAsync( ExecSomeAction ) + .BuildAsProcedure(); } - // usage - void usage( IMyCommand command ) + private String ExecSomeAction( IPipelineContext context, String who ) { - var result = await command.ExecuteAsync( "me" ); // returns "Hello me" + return $"Hello {who}"; } +} + +// usage +void usage( IMyCommand command ) +{ + var result = await command.ExecuteAsync( "me" ); // returns "Hello me" +} ``` \ No newline at end of file diff --git a/docs/index.md b/docs/index.md index 8a380dd..cfa2859 100644 --- a/docs/index.md +++ b/docs/index.md @@ -53,14 +53,14 @@ Pipelines are built using `PipelineFactory`. Once built, a pipeline is just an a an optional input value as parameters, and returns a result. ```csharp - var command = PipelineFactory - .Start() - .Pipe( ( ctx, arg ) => $"hello {arg}" ) - .Build(); +var command = PipelineFactory + .Start() + .Pipe( ( ctx, arg ) => $"hello {arg}" ) + .Build(); - var result = await command( new PipelineContext(), "pipeline" ); +var result = await command( new PipelineContext(), "pipeline" ); - Assert.AreEqual( "hello pipeline", result ); +Assert.AreEqual( "hello pipeline", result ); ``` ## Dependency Injection diff --git a/docs/middleware.md b/docs/middleware.md index ff23789..6fecc8a 100644 --- a/docs/middleware.md +++ b/docs/middleware.md @@ -100,7 +100,6 @@ var command = PipelineFactory var result = await command( new PipelineContext() ); Assert.AreEqual( "{12}3", result ); - ``` ### Example @@ -144,7 +143,9 @@ _output:_ [02] begin transaction (name = 'T') [02] end transaction (name = 'T') ``` -The `WithTransaction` wrapped all the pipeline steps and was only executed at the beginning and and of the command with the `next` method being the entire group of actions. + +The `WithTransaction` wrapped all the pipeline steps and was only executed at the beginning and and of the command with the +`next` method being the entire group of actions. ## Composition