-
Notifications
You must be signed in to change notification settings - Fork 2
AutoProcessor
AutoProcessor is the core feature of AutoPipe. It represents a processing unit that automatically maps values from the Bag into strongly typed properties and methods of your processor class. This significantly reduces boilerplate code and makes pipeline development faster and more maintainable.
Unlike a traditional processor, where you manually read and write values from the Bag
, an AutoProcessor
leverages conventions and attributes to bind data, execute logic, and push results back into the pipeline.
Key benefits:
-
Automatic binding — input values from the
Bag
are mapped to processor methods. - Minimal boilerplate — you only implement the business logic, AutoPipe handles data flow.
- Integration with Bag API — full access to messages, errors, results, and pipeline state.
- Extensibility — you can configure how properties map, how defaults are applied, and when the processor should execute.
- Fluent pipeline composition — easily combine multiple processors into a single pipeline.
Typical use cases:
- Validation of input data.
- Data transformation and enrichment.
- External service calls (e.g., APIs, databases).
- Calculation and aggregation.
- Preparing and shaping final results.
You can create a processor in AutoPipe in two main ways: inheritance from AutoProcessor
or wrapping an existing class. Each approach has its own benefits.
Creating a processor by inheriting from AutoProcessor
provides built-in integration with the Bag
, automatic method execution, and access to all helper methods like ErrorHalt()
, Info()
, etc.
public class ValidateOrderProcessor : AutoProcessor
{
public object ValidateOrderId(int orderId)
{
if (orderId <= 0)
return this.ErrorHalt("OrderId is missing");
return this.Info("OrderId is valid");
}
}
- Public methods like
ValidateOrderId(int orderId)
are automatically executed in sequence by the processor. - Method parameters can be automatically mapped from the Bag if the name matches a
Bag
key. - Inside methods, you can read and modify values, add messages, or stop the pipeline. See Bag API for details.
- Inheritance allows you to use all built-in
AutoProcessor
methods, making your processor more concise and easier to maintain.
You don’t have to inherit from AutoProcessor
directly. Any class with public methods can be wrapped into an AutoProcessor
:
public class ValidateOrder
{
public void ValidateOrderId(Bag bag)
{
if (!bag.ContainsKey("OrderId"))
bag.Error("OrderId is missing").Halt();
}
}
var logic = new ValidateOrder();
var processor = new AutoProcessor(logic);
This makes it easy to turn plain classes into pipeline-ready processors.
AutoPipe provides factory methods to simplify processor creation:
-
AutoProcessor.From<T>()
- creates a processor using parameterless constructor
var processor = AutoProcessor.From<ValidateOrder>();
-
AutoProcessor.From(object processorClass)
- creates a processor from an existing instance
var processor = AutoProcessor.From(new ValidateOrder());
By default, AutoProcessor executes all public methods in the order they are defined, unless dependencies are detected.
Example:
public class Test : AutoProcessor
{
public void Step2()
{
// Executed first because it's declared first
Console.WriteLine("Step2");
}
public void Step1()
{
// Executed second
Console.WriteLine("Step1");
}
}
new Test().RunSync();
// OUTPUT:
// Step2
// Step1
Methods are executed in the order they are declared in the class, unless dependencies or execution attributes specify otherwise.
AutoProcessor
can automatically detect method dependencies based on parameter types and return values:
If a method requires a value that is not present in the Bag, and another method produces it, the producing method is executed first.
Example:
public class Test : AutoProcessor
{
public void WriteMessage(string message)
{
Console.WriteLine("WriteMessage");
Console.WriteLine(message);
}
public string GetMessage()
{
Console.WriteLine("GetMessage");
return "Hello world!";
}
}
new Test().RunSync();
// OUTPUT:
// GetMessage
// WriteMessage
// Hello world!
Here
GetMessage()
is executed first to provide the message parameter forWriteMessage
.
You can fine-tune which methods run and in what order using attributes:
-
[Run]
— run only this method, ignoring the default rule of executing all public methods.
public class Test : AutoProcessor
{
public void Step1()
{
Console.WriteLine("Step1"); // This method will be skipped
}
[Run]
private void Step2()
{
// Only this method will run because of [Run]
Console.WriteLine("Step2");
}
}
new Test().RunSync();
// OUTPUT:
// Step2
[Run]
disables the default execution of all public methods and runs only the marked method.
-
[RunAll]
— run all methods in the class, including non-public and static.
[RunAll] // Non-private and private methods will all run
public class Test : AutoProcessor
{
public static void Step1()
{
Console.WriteLine("Step1"); // Static method will run
}
private void Step2()
{
Console.WriteLine("Step2"); // Private method will run
}
}
new Test().RunSync();
// OUTPUT:
// Step1
// Step2
[RunAll]
executes all methods, including private and static ones.
-
[Skip]
— skip this method from execution.
[RunAll]
public class Test : AutoProcessor
{
[Skip]
public static void Step1()
{
Console.WriteLine("Step1"); // Skipped because of [Skip]
}
private void Step2()
{
Console.WriteLine("Step2"); // Only this method runs
}
}
new Test().RunSync();
// OUTPUT:
// Step2
[Skip]
allows you to exclude methods from execution, even when[RunAll]
is applied.
-
[Order(int order)]
- specify a custom execution order.
[RunAll]
public class Test : AutoProcessor
{
[Order(2)]
private void Step2()
{
Console.WriteLine("Step2"); // Executed second
}
[Order(1)]
public static void Step1()
{
Console.WriteLine("Step1"); // Executed first
}
}
new Test().RunSync();
// OUTPUT:
// Step1
// Step2
[Order]
explicitly defines the execution order of methods.
-
[After(string name)]
- execute this method after the specified one.
[RunAll]
public class Test : AutoProcessor
{
[After("Step1")]
private void Step2()
{
Console.WriteLine("Step2"); // Runs after Step1
}
public static void Step1()
{
Console.WriteLine("Step1"); // Runs first
}
}
new Test().RunSync();
// OUTPUT:
// Step1
// Step2
[After] ensures that a method runs after the specified method, useful for dependency management.
These attributes provide fine-grained control over execution, enabling precise pipeline behavior and handling complex dependencies.
AutoProcessor automatically maps method parameters from the Bag
by their names (case-insensitive).
public class Test : AutoProcessor
{
public void PrintInformation(string name, int age)
{
Console.WriteLine($"{name} is {age} years old");
}
}
var bag = new Bag { ["Name"] = "Alice", ["Age"] = 30 };
new Test().RunSync(bag);
// OUTPUT:
// Alice is 30 years old
Available attributes for parameter handling:
-
[Or(value)]
— provide a fallback default if the parameter is missing. -
[Aka("alias1", "alias2", ...)]
— map parameters to alternative names in the Bag. -
[Required]
— skip method execution if a parameter is missing (with optional Halt or Error). -
[Strict]
— enforce that all parameters must be present (method/class level).
If a parameter is not found in the Bag, its type’s default value is used:
public class Test : AutoProcessor
{
public void PrintInformation(bool writeInformation, string name, int age)
{
if (writeInformation) // default value is false
Console.WriteLine($"{name} is {age} years old");
}
}
var bag = new Bag { ["Name"] = "Alice", ["Age"] = 30 };
new Test().RunSync(bag);
// NO OUTPUT
You can override this behavior with the [Or(object value)]
attribute:
public class Test : AutoProcessor
{
public void PrintInformation([Or(true)] bool writeInformation, string name, int age)
{
if (writeInformation) // default value is true from [Or] attribute
Console.WriteLine($"{name} is {age} years old");
}
}
var bag = new Bag { ["Name"] = "Alice", ["Age"] = 30 };
new Test().RunSync(bag);
// OUTPUT
// Alice is 30 years old
[Or(value)]
— supplies a custom fallback value if the parameter is missing in the Bag.
If the Bag contains data under a different key, you can use [Aka(string name, params string[] aliases)]
to declare alternative names:
public class ShowCommodityInformation : AutoProcessor
{
public void Print([Aka("Title", "Product")] string name)
{
Console.WriteLine($"{name} is at the warehouse");
}
}
var processor = new ShowCommodityInformation();
processor.RunSync(new { Name = "Vacuum cleaner" });
processor.RunSync(new { Title = "Speaker" });
processor.RunSync(new { Product = "Video camera" });
// OUTPUT
// Vacuum cleaner is at the warehouse
// Speaker is at the warehouse
// Video camera is at the warehouse
Use [Required]
to prevent method execution if a parameter is missing.
-
[Required(Halt = true)]
stops the whole pipeline. -
[Required(Error = "...")]
attaches an error message for debugging.
public class ApplyDiscount : AutoProcessor
{
public decimal UpdateTotal(decimal total, [Required] decimal discountPercentage)
{
return total - (total * discountPercentage / 100);
}
}
Here, if
discountPercentage
is missing, the method will not run and no discount will be applied.
[Strict]
can be applied to a method or a whole class. It enforces that all parameters must be present in the Bag. If any parameter is missing, the method is skipped:
public class CreateProduct : AutoProcessor
{
[Strict]
public void AddToDatabase(string id, string name, decimal price)
{
AddProductToDB(id, name, price);
}
}
This system gives you precise control over parameter resolution and validation without writing boilerplate checks.
AutoProcessor methods can return many different things. Each return type is automatically processed and merged into the Bag, so you don’t need extra code to wire values together.
When you return an object, its properties are added to the Bag as keys.
public class RetrieveProduct : AutoProcessor
{
public object ComposeProduct()
{
// Each property here becomes a Bag key
return new { Id = 123, Name = "Laptop" };
}
}
var bag = new RetrieveProduct().RunSync();
Console.WriteLine($"{bag["name"]} has ID {bag["id"]}");
// OUTPUT: Laptop has ID 123
If the method name starts with Get
, the whole object is stored under that name.
public class RetrieveProduct : AutoProcessor
{
public object GetProduct()
{
// Stored as bag["product"]
return new { Id = 123, Name = "Smartphone" };
}
}
var bag = new RetrieveProduct().RunSync();
dynamic product = bag["product"];
Console.WriteLine($"Product {product.Name} has ID {product.Id}");
// OUTPUT: Product Smartphone has ID 123
Async methods are awaited automatically — results are processed just like objects.
public class RetrieveProduct : AutoProcessor
{
public async Task<object> GetProduct()
{
await Task.Delay(50); // simulate async work
return new { Id = 999, Name = "Headphones" };
}
}
var bag = await new RetrieveProduct().Run(); // processor can be awaited outside
Console.WriteLine($"Product {bag["name"]} has ID {bag["id"]}");
// OUTPUT: Product Headphones has ID 999
You can return multiple items. Each element is merged into the Bag in sequence.
public class RetrieveProduct : AutoProcessor
{
public IEnumerable LoadProductInfo()
{
// First adds Name
yield return new { Name = "Keyboard" };
// Then adds Id (Task is also supported)
yield return Task.FromResult(new { Id = 456 });
}
}
var bag = new RetrieveProduct().RunSync();
Console.WriteLine($"{bag["name"]} has ID {bag["id"]}");
// OUTPUT: Keyboard has ID 456
If you return Func<Bag, object>
, it is executed with the current Bag, letting you compute values dynamically.
public class RetrieveProduct : AutoProcessor
{
public Func<Bag, object> CalculatePrice()
{
return bag =>
{
var discount = bag.Int("discountPercent");
var basePrice = 1000;
return new { FinalPrice = basePrice - (basePrice * discount / 100) };
};
}
}
var bag = new RetrieveProduct().RunSync(new { DiscountPercent = 20 });
Console.WriteLine($"Final price is {bag["finalPrice"]}");
// OUTPUT: Final price is 800
If you return Action<Bag>
, it is executed directly on the Bag. Perfect for logging or side-effects.
public class Logger : AutoProcessor
{
public Action<Bag> LogInfo(string message)
{
return bag => bag.Info($"[LOG] {message}");
}
}
var bag = new Logger().RunSync(new { Message = "Pipeline started" });
// OUTPUT in Bag Messages: [LOG] Pipeline started
AutoProcessor provides helper action methods you can call inside your processors. These methods modify the Bag, add messages, or control pipeline flow.
- Messages
public class Messages : AutoProcessor
{
public IEnumerable Demo()
{
yield return Info("Everything is fine");
yield return Warning("Something looks odd");
yield return Error("Something went wrong");
}
}
- Control flow
public class Control : AutoProcessor
{
public object StopIfInvalid(bool valid)
{
if (!valid)
return ErrorHalt("Validation failed"); // adds error and stops pipeline
}
}
- Returning results
public class Checkout : AutoProcessor
{
public object CalculateTotal(decimal subtotal)
{
var total = subtotal * 1.2m; // tax
return Result(total); // puts "result" into Bag
}
}
- Combined: result + message + halt
public class Checkout : AutoProcessor
{
public object ValidateCoupon(string code)
{
if (code == "EXPIRED")
return WarningHaltResult(null, "Coupon expired");
}
}
With these mechanisms, you can return data, tasks, collections, functions, or actions, and AutoProcessor will seamlessly integrate everything into your pipeline.
AutoProcessor can detect certain prefixes in method names and apply special logic automatically.
Prefix | Behavior |
---|---|
Get / Ensure / Add
|
Adds value to Bag only if it doesn’t exist |
Set / Update / Overwrite
|
Adds or replaces the value in the Bag |
public class Product : AutoProcessor
{
public string GetName() => "Tablet"; // stored as bag["name"], only if missing
public string SetName() => "Monitor"; // overwrites bag["name"]
}
var bag = new Product().RunSync();
Console.WriteLine(bag["name"]);
// OUTPUT: Monitor
If you don’t want any special handling based on method names, you can disable it:
public class Product : AutoProcessor
{
public Product()
{
SkipNameBasedActions = true; // disable special name keywords
}
}
You can also override how names are interpreted and enforce your own rules:
public class BaseProcessor : AutoProcessor
{
protected override bool ProcessBasedOnName(MethodInfo method, Bag context, object methodResult)
{
if (method.Name.StartsWith("ThrowIfEmpty") && methodResult == null)
{
throw new ArgumentException($"Method {method.Name} returned null.");
}
return false; // false = nothing special was processed
}
}
Sometimes you might not understand why a certain method is not executed or a processor is skipped.
For this purpose, Bag
has a Debug
option that collects useful information during method execution.
[RunAll]
public class Test : AutoProcessor
{
[Order(2)]
private void Step2()
{
// Executed second
}
[Order(1)]
public static void Step1()
{
// Executed first
}
}
var bag = new Bag { Debug = true };
new Test().RunSync(bag);
Console.WriteLine(bag.Summary());
Output (with Debug enabled):
Running processor [Test].
Verifying parameters of method [Step1].
All parameters are valid. Running method [Step1].
Completed method [Step1].
Verifying parameters of method [Step2].
All parameters are valid. Running method [Step2].
Completed method [Step2].
Processor [Test] completed.
Note: Setting
Debug = true
is the easiest way to understand why a method was skipped or why a processor did not run as expected.
You can enrich the debug logs with human-friendly names and descriptions. Two attributes are available:
-
[Is(string description)]
– adds a description to a processor, method, or parameter. -
[Aka(params string[] aliases)]
– adds alternative names.
[RunAll]
[Aka("MessageHandler")]
[Is("a message writer")]
public class Test : AutoProcessor
{
[Order(2)]
[Aka("Next Step")]
[Is("the step that runs after a message is displayed")]
private void Step2()
{
}
[Order(1)]
[Aka("Message Writer")]
[Is("a method that displays a message")]
public static void Step1(
[Aka("Text")]
[Is("the message text to be displayed")]
[Required] string message)
{
// This method will not run because "message" is missing in the Bag
}
}
var bag = new Bag { Debug = true };
new Test().RunSync(bag);
Console.WriteLine(bag.Summary());
Output with attributes:
Running processor [MessageHandler] that is a message writer
Verifying parameters of method [Message Writer]. Method is a method that displays a message
Property [message] is not found. Skipping method [Message Writer] in [MessageHandler].
Method [Message Writer] cannot be run. Going to the next one.
Verifying parameters of method [Next Step]. Method is the step that runs after a message is displayed
All parameters are valid. Running method [Next Step].
Completed method [Next Step].
Processor [MessageHandler] completed.
AutoProcessor
is the core component of AutoPipe that transforms ordinary classes and methods into flexible, declarative processors.
-
Inheritance or Wrapper – inherit from
AutoProcessor
or use it directly. -
Automatic Parameter Binding – values flow through the
Bag
without manual wiring. -
Flexible Return Types – objects, async tasks, delegates, collections — all seamlessly integrated into the
Bag
. -
Method Name Keywords – prefixes like
Get
,Set
,Ensure
control how results are stored. -
Built-in Result & Message Helpers –
Info
,Error
,Halt
,Result
simplify step logic. -
Declarative Attributes –
[Order]
,[RunAll]
,[Aka]
,[Is]
,[Required]
make execution rules explicit. - Debug Mode – transparently shows which steps were executed or skipped.
It helps you build clean, modular, and easily debuggable pipelines where each method becomes a well-defined step, and business logic is expressed declaratively.