Skip to main content

Context Inheritance

Overview

Context inheritance controls what data from the parent pipeline's context is available to sub-pipelines. This is a critical design decision that affects isolation, testability, and behavior of your composite pipelines.

Context Components

The PipelineContext has three main dictionaries:

Parameters

Used for pipeline input parameters and configuration:

context.Parameters["DatabaseConnection"] = connectionString;
context.Parameters["BatchSize"] = 100;

Typical Use Cases:

  • Configuration values
  • Connection strings
  • Processing parameters
  • Input data for composite nodes

Items

Used for request-scoped state and services:

context.Items["Logger"] = myLogger;
context.Items["RequestId"] = Guid.NewGuid();

Typical Use Cases:

  • Request-scoped services
  • Temporary state
  • Request identifiers
  • Cross-cutting concerns

Properties

Used for metadata and pipeline-level configuration:

context.Properties["Environment"] = "Production";
context.Properties["Version"] = "1.0.0";

Typical Use Cases:

  • Pipeline metadata
  • Environment settings
  • Feature flags
  • Global configuration

Inheritance Strategies

No Inheritance (Default)

Configuration:

builder.AddComposite<TIn, TOut, SubPipeline>(
contextConfiguration: CompositeContextConfiguration.Default);

When to Use:

  • Sub-pipeline should be completely isolated
  • Testing sub-pipelines independently
  • Avoiding unintended dependencies
  • Maximum modularity

Characteristics:

  • Sub-pipeline has empty context dictionaries
  • No parent data accessible
  • Complete isolation
  • Easiest to test

Example:

public class StandaloneValidationPipeline : IPipelineDefinition
{
public void Define(PipelineBuilder builder, PipelineContext context)
{
// This pipeline doesn't need any parent context
var input = builder.AddSource<PipelineInputSource<Customer>, Customer>("input");
var validate = builder.AddTransform<BasicValidator, Customer, ValidatedCustomer>("validate");
var output = builder.AddSink<PipelineOutputSink<ValidatedCustomer>, ValidatedCustomer>("output");

builder.Connect(input, validate);
builder.Connect(validate, output);
}
}

Full Inheritance

Configuration:

builder.AddComposite<TIn, TOut, SubPipeline>(
contextConfiguration: CompositeContextConfiguration.InheritAll);

When to Use:

  • Sub-pipeline needs access to parent configuration
  • Sharing services across pipeline hierarchy
  • Consistent environment settings
  • Logging and tracing integration

Characteristics:

  • All parent context data copied to sub-context
  • Parent context remains isolated from changes
  • Sub-pipeline can read parent values
  • More complex testing requirements

Example:

public class ConfigAwareEnrichmentPipeline : IPipelineDefinition
{
public void Define(PipelineBuilder builder, PipelineContext context)
{
// Access parent configuration
var apiKey = context.Parameters["ApiKey"]?.ToString() ?? "";
var logger = context.Items["Logger"] as ILogger;

var input = builder.AddSource<PipelineInputSource<Customer>, Customer>("input");
var enrich = builder.AddTransform<ApiEnricher, Customer, EnrichedCustomer>("enrich");
var output = builder.AddSink<PipelineOutputSink<EnrichedCustomer>, EnrichedCustomer>("output");

builder.Connect(input, enrich);
builder.Connect(enrich, output);
}
}

// Usage in parent
var context = new PipelineContext();
context.Parameters["ApiKey"] = "secret-key";
context.Items["Logger"] = myLogger;

builder.AddComposite<Customer, EnrichedCustomer, ConfigAwareEnrichmentPipeline>(
contextConfiguration: CompositeContextConfiguration.InheritAll);

Selective Inheritance

Configuration:

builder.AddComposite<TIn, TOut, SubPipeline>(
contextConfiguration: new CompositeContextConfiguration
{
InheritParentParameters = true,
InheritParentItems = false,
InheritParentProperties = true
});

When to Use:

  • Need specific parent data only
  • Balance between isolation and access
  • Fine-grained control over dependencies
  • Performance optimization

Example:

// Sub-pipeline needs config but not services
builder.AddComposite<Order, ProcessedOrder, OrderProcessingPipeline>(
contextConfiguration: new CompositeContextConfiguration
{
InheritParentParameters = true, // Config values
InheritParentItems = false, // No services
InheritParentProperties = true // Metadata
});

Custom Configuration with Action

Configuration:

builder.AddComposite<TIn, TOut, SubPipeline>(
configureContext: config =>
{
config.InheritParentParameters = shouldInheritParams;
config.InheritParentItems = shouldInheritItems;
config.InheritParentProperties = shouldInheritProps;
});

When to Use:

  • Dynamic configuration based on conditions
  • Configuration from external sources
  • Complex inheritance logic

Example:

var isDevelopment = Environment.GetEnvironmentVariable("ENVIRONMENT") == "Development";

builder.AddComposite<TIn, TOut, SubPipeline>(
configureContext: config =>
{
config.InheritParentParameters = true;
config.InheritParentItems = isDevelopment; // Only in dev
config.InheritParentProperties = true;
});

Isolation and Safety

Parent Context is Always Isolated

Changes in sub-pipeline context never affect parent context:

// Parent pipeline
var context = new PipelineContext();
context.Parameters["SharedValue"] = "Original";

// Run composite with inheritance
await runner.RunAsync<ParentPipeline>(context);

// Parent value unchanged, even if sub-pipeline modified it
Assert.Equal("Original", context.Parameters["SharedValue"]);

Sub-Pipeline Gets Copies

When inheritance is enabled, sub-pipeline receives copies of the dictionaries:

// In sub-pipeline transform
public override Task<T> ExecuteAsync(T input, PipelineContext context, ...)
{
// This modifies the sub-pipeline's copy only
context.Parameters["SharedValue"] = "Modified";

// Parent's value remains unchanged
return Task.FromResult(input);
}

Performance Considerations

Memory Overhead

Inheritance involves copying dictionaries:

ConfigurationMemory Impact
Default (no inheritance)Minimal - empty dictionaries
InheritAllModerate - copies all three dictionaries
SelectiveLow to moderate - copies selected dictionaries

Recommendation: Only inherit what you need.

Copy Timing

Dictionaries are copied once per item when the composite node processes it:

// For each item from source:
// 1. Create sub-context (with copies if inheritance enabled)
// 2. Execute sub-pipeline
// 3. Retrieve output
// 4. Discard sub-context

Recommendation: For high-throughput scenarios, prefer no inheritance.

Common Patterns

Pattern 1: Configuration Inheritance

Pass configuration to sub-pipelines:

// Parent sets up config
var context = new PipelineContext();
context.Parameters["ApiEndpoint"] = "https://api.example.com";
context.Parameters["Timeout"] = TimeSpan.FromSeconds(30);

// Sub-pipeline reads config
builder.AddComposite<TIn, TOut, ApiCallPipeline>(
contextConfiguration: new CompositeContextConfiguration
{
InheritParentParameters = true
});

Pattern 2: Service Injection

Share services across pipeline hierarchy:

// Parent sets up services
var context = new PipelineContext();
context.Items["DatabaseConnection"] = dbConnection;
context.Items["Cache"] = cache;

// Sub-pipeline uses services
builder.AddComposite<TIn, TOut, DatabasePipeline>(
contextConfiguration: new CompositeContextConfiguration
{
InheritParentItems = true
});

Pattern 3: Environment Context

Share environment settings:

// Parent sets environment
var context = new PipelineContext();
context.Properties["Environment"] = "Production";
context.Properties["Region"] = "US-West";

// Sub-pipeline adapts to environment
builder.AddComposite<TIn, TOut, AdaptivePipeline>(
contextConfiguration: new CompositeContextConfiguration
{
InheritParentProperties = true
});

Pattern 4: Isolated Testing

Test sub-pipelines independently:

[Fact]
public async Task SubPipeline_WithTestData_ShouldProcess()
{
// Test sub-pipeline directly with test context
var context = new PipelineContext();
context.Parameters["TestMode"] = true;

var runner = PipelineRunner.Create();
await runner.RunAsync<MySubPipeline>(context);

// Verify behavior
}

Best Practices

1. Default to No Inheritance

Start with no inheritance and add it only when needed:

✅ Good: Start simple
builder.AddComposite<TIn, TOut, SubPipeline>(); // Uses Default

// Add inheritance only if needed
builder.AddComposite<TIn, TOut, SubPipeline>(
contextConfiguration: new CompositeContextConfiguration
{
InheritParentParameters = true // Only what's needed
});

2. Document Dependencies

Clearly document what context data a sub-pipeline needs:

/// <summary>
/// Processes orders using external API.
/// </summary>
/// <remarks>
/// Required Parameters:
/// - "ApiKey" (string): API authentication key
/// - "Timeout" (TimeSpan): Request timeout
///
/// Required Items:
/// - "Logger" (ILogger): Logging service
/// </remarks>
public class ApiOrderProcessingPipeline : IPipelineDefinition
{
// ...
}

3. Use Type-Safe Accessors

Create helper methods for accessing context:

public static class ContextExtensions
{
public static string GetApiKey(this PipelineContext context)
{
return context.Parameters.TryGetValue("ApiKey", out var value)
? value?.ToString() ?? throw new InvalidOperationException("ApiKey not found")
: throw new InvalidOperationException("ApiKey not found");
}

public static ILogger GetLogger(this PipelineContext context)
{
return context.Items.TryGetValue("Logger", out var value)
? value as ILogger ?? throw new InvalidOperationException("Logger not found")
: throw new InvalidOperationException("Logger not found");
}
}

// Usage
var apiKey = context.GetApiKey();
var logger = context.GetLogger();

4. Test Both With and Without Inheritance

Test sub-pipelines in both modes:

[Fact]
public async Task SubPipeline_Standalone_ShouldWork()
{
// Test without parent context
var context = new PipelineContext();
await runner.RunAsync<SubPipeline>(context);
}

[Fact]
public async Task SubPipeline_WithParentContext_ShouldInherit()
{
// Test with parent context
var parentContext = new PipelineContext();
parentContext.Parameters["Config"] = "value";

await runner.RunAsync<ParentPipeline>(parentContext);
}

5. Avoid Implicit Dependencies

Make dependencies explicit through parameters or constructor injection:

❌ Bad: Hidden dependency
public class MyTransform : TransformNode<T, T>
{
public override Task<T> ExecuteAsync(T input, PipelineContext context, ...)
{
// Implicitly requires "Config" in context
var config = context.Parameters["Config"];
// ...
}
}

✅ Good: Explicit dependency
public class MyTransform : TransformNode<T, T>
{
private readonly string _config;

public MyTransform(PipelineContext context)
{
if (!context.Parameters.TryGetValue("Config", out var value))
throw new ArgumentException("Config parameter is required");
_config = value.ToString() ?? throw new ArgumentException("Config cannot be null");
}

public override Task<T> ExecuteAsync(T input, PipelineContext context, ...)
{
// Use _config
}
}

Summary

StrategyParametersItemsPropertiesUse Case
DefaultIsolated, testable sub-pipelines
InheritAllFull integration with parent
Parameters OnlyConfiguration inheritance
Items OnlyService sharing
Properties OnlyMetadata/environment
Custom🔧🔧🔧Fine-grained control

Choose the strategy that best balances isolation, functionality, and testability for your specific use case.