Skip to content

AutoProcessor

Sergey Solomentsev edited this page Sep 24, 2025 · 1 revision

AutoProcessor API

Overview

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.

Creating a Processor

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.

Inheritance

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.

Wrapping an Existing Class

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.

Factory Methods

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());

Method Execution Rules

Default execution order

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.

Dependency-Aware Execution

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 for WriteMessage.

Controlling Execution with Attributes

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.

Parameter Binding and Validation

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).

Default values

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.

Aliases

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

Required parameters

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 mode

[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.

Returning Values from Methods

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.

Anonymous Objects

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

Named Objects

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

Tasks (Async)

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

Collections

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

Functions

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

Actions

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

Built-in Result & Message Methods

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.

Special Method Name Keywords

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

Disabling Name-based Actions

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
    }
}

Customizing the Behavior

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
    }
}

Debug

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.

Adding more clarity with attributes

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.

Summary

AutoProcessor is the core component of AutoPipe that transforms ordinary classes and methods into flexible, declarative processors.

Key Features:

  • 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 HelpersInfo, 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.

Why Use AutoProcessor?

It helps you build clean, modular, and easily debuggable pipelines where each method becomes a well-defined step, and business logic is expressed declaratively.