Runnable 1.0.0

dotnet add package Runnable --version 1.0.0
                    
NuGet\Install-Package Runnable -Version 1.0.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Runnable" Version="1.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Runnable" Version="1.0.0" />
                    
Directory.Packages.props
<PackageReference Include="Runnable" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Runnable --version 1.0.0
                    
#r "nuget: Runnable, 1.0.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Runnable@1.0.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Runnable&version=1.0.0
                    
Install as a Cake Addin
#tool nuget:?package=Runnable&version=1.0.0
                    
Install as a Cake Tool

Runnable ๐Ÿš€

.NET C# License

Runnable is a powerful, composable pipeline library for .NET inspired by LangChain's LCEL (LangChain Expression Language). Build sophisticated data processing pipelines with a clean, functional API that supports multi-tenancy, distributed tracing, caching, and resilience patterns.

โœจ Features

  • ๐Ÿ”— Composable Pipelines - Chain operations with fluent API
  • ๐Ÿข Multi-Tenant Support - Built-in tenant, user, and correlation ID tracking
  • โšก Context-Aware Operations - Map, filter, branch, and cache based on execution context
  • ๐Ÿ”„ Retry & Resilience - Exponential backoff, circuit breaker, timeout support
  • ๐Ÿ’พ Advanced Caching - Per-tenant/user caching with TTL and LRU eviction
  • ๐Ÿ“Š Distributed Tracing - Correlation IDs and trace propagation
  • ๐ŸŽฏ Type-Safe - Full generic support for 0-16 parameters
  • โš™๏ธ Async-First - Native async/await support throughout
  • ๐Ÿงช A/B Testing - Built-in experimentation and feature flags
  • ๐Ÿ“ฆ .NET Standard 2.0+ - Compatible with .NET 5, 6, 8, 9, 10

๐Ÿ“ฆ Installation

dotnet add package Runnable

๐Ÿš€ Quick Start

Basic Pipeline

using Runnable;

// Create a simple pipeline
var pipeline = RunnableLambda.Create<string, string>(input => input.ToUpper())
    .Map(upper => $"Hello, {upper}!")
    .Tap(result => Console.WriteLine(result));

var result = pipeline.Invoke("world");
// Output: "HELLO, WORLD!"

Async Pipeline with Retry

var apiPipeline = RunnableLambda.Create<string, Task<ApiResponse>>(async url =>
    await httpClient.GetFromJsonAsync<ApiResponse>(url))
    .WithExponentialBackoff(maxRetries: 3)
    .WithTimeout(TimeSpan.FromSeconds(30))
    .Map(response => response.Data)
    .WithCache();

var data = await apiPipeline.InvokeAsync("https://api.example.com/data");

Branching Logic

var processor = RunnableLambda.Create<Order, Order>(order => order)
    .Branch(
        order => order.Total > 1000,
        premiumProcessor,  // For high-value orders
        standardProcessor  // For regular orders
    );

๐Ÿข Multi-Tenant & Context-Aware Pipelines

Runnable provides powerful context-aware operations perfect for multi-tenant SaaS applications:

Setting Context

var pipeline = apiService.GetData()
    .WithTenant(request.Headers["X-Tenant-ID"])
    .WithUser(request.User.Id)
    .WithCorrelationId(request.Headers["X-Correlation-ID"]);

Context-Aware Filtering

// Automatic tenant isolation
var tenantData = dataService.GetAll()
    .FilterContext((data, ctx) => data.TenantId == ctx.TenantId);

Context-Aware Mapping

// Enrich data with context information
var enriched = processor.Process()
    .MapContext((result, ctx) => new Response {
        Data = result,
        TenantId = ctx.TenantId,
        UserId = ctx.UserId,
        CorrelationId = ctx.CorrelationId,
        ProcessedAt = DateTime.UtcNow
    });

Context-Based Routing

// Route premium tenants to optimized handler
var handler = defaultHandler
    .BranchByTenant("premium", premiumHandler)
    .BranchByTenant("enterprise", enterpriseHandler);

// A/B testing
var experiment = defaultHandler
    .ABTestContext("new_feature", variantHandler);

// Debug mode routing
var pipeline = productionHandler
    .BranchByDebugMode(debugHandler);

Per-Tenant Caching ๐Ÿ”ฅ

// Each tenant gets isolated cache
var service = expensiveOperation
    .WithCachePerTenant();

// Per-tenant cache with TTL
var catalog = getCatalog
    .WithCachePerTenantTTL(TimeSpan.FromMinutes(15));

// Per-tenant cache with LRU (memory-bounded)
var products = getProducts
    .WithCachePerTenantLRU(maxSize: 1000);

// Ultimate: TTL + LRU per tenant
var analytics = getAnalytics
    .WithCachePerTenantTTLAndLRU(
        ttl: TimeSpan.FromMinutes(10),
        maxSize: 500);

๐ŸŽฏ Real-World Example

Complete Multi-Tenant SaaS Pipeline

var metrics = new MetricsCollector();
var logger = new Logger();

var pipeline = apiService
    .GetRequest()
    
    // 1. Set execution context
    .WithTenant(request.Headers["X-Tenant-ID"])
    .WithUser(request.User.Id)
    .WithCorrelationId(request.Headers["X-Correlation-ID"])
    
    // 2. Security: Filter by tenant
    .FilterContext((req, ctx) => 
        req.TenantId == ctx.TenantId)
    
    // 3. Route premium tenants
    .BranchByTenant("premium", premiumProcessor)
    
    // 4. Process data
    .Map(data => transformer.Transform(data))
    
    // 5. Enrich with context
    .MapContext((result, ctx) => new Response {
        Data = result,
        TenantId = ctx.TenantId,
        UserId = ctx.UserId,
        CorrelationId = ctx.CorrelationId
    })
    
    // 6. Cache per tenant
    .WithCachePerTenantTTLAndLRU(
        ttl: TimeSpan.FromMinutes(5),
        maxSize: 1000)
    
    // 7. Observability
    .TapContext((response, ctx) =>
        logger.LogInfo($"Processed for tenant {ctx.TenantId}"))
    .WithMetrics(metrics)
    
    // 8. Resilience
    .WithExponentialBackoff(maxRetries: 3)
    .WithTimeout(TimeSpan.FromSeconds(30));

var result = await pipeline.InvokeAsync(request);

๐Ÿ“š Core Concepts

Runnable Types

Type Description Example
RunnableLambda Wraps any function/lambda RunnableLambda.Create<int, int>(x => x * 2)
RunnableMap Transforms input to output input.Map(x => x.ToString())
RunnableBranch Conditional routing input.Branch(predicate, trueHandler, falseHandler)
RunnablePassthrough Passes input through unchanged RunnablePassthrough.Create<T>()

Extension Methods

Transformation
  • .Map(func) - Transform output
  • .MapAsync(func) - Async transformation
  • .MapContext(func) - Transform with context access
Filtering
  • .Filter(predicate) - Filter based on condition
  • .FilterContext(predicate) - Filter with context
  • .FilterOrDefault(predicate, default) - Filter with fallback
Branching
  • .Branch(predicate, truePath, falsePath) - Conditional routing
  • .BranchContext(predicate, branch) - Context-aware routing
  • .BranchByTenant(tenantId, handler) - Tenant-specific routing
  • .ABTestContext(experimentKey, variant) - A/B testing
Observability
  • .Tap(action) - Side effect without changing output
  • .TapAsync(action) - Async side effect
  • .TapContext(action) - Observe with context access
  • .WithMetrics(collector) - Collect metrics
  • .WithTelemetry(tracer) - Distributed tracing
Caching
  • .WithCache() - Simple memoization
  • .WithCacheTTL(timespan) - Cache with expiration
  • .WithCachePerTenant() - Per-tenant isolation
  • .WithCachePerUser() - Per-user isolation
  • .WithCachePerTenantTTLAndLRU(ttl, maxSize) - Advanced caching
Resilience
  • .WithRetry(maxAttempts) - Simple retry
  • .WithExponentialBackoff(maxRetries) - Exponential backoff
  • .WithTimeout(timespan) - Execution timeout
  • .WithCircuitBreaker(threshold) - Circuit breaker pattern
  • .WithFallback(handler) - Fallback on error
Context
  • .WithContext(key, value) - Set context value
  • .WithCorrelationId(id) - Set correlation ID
  • .WithTenant(tenantId) - Set tenant context
  • .WithUser(userId) - Set user context

๐ŸŽจ Advanced Patterns

Parallel Processing

var pipeline = RunnableLambda.Create<List<Item>, List<Result>>(items =>
    items.AsParallel()
         .Select(item => processor.Invoke(item))
         .ToList());

Batch Processing

var batchProcessor = RunnableLambda.Create<Item, Result>(item => 
    Process(item));

var results = batchProcessor.Batch(items);

Streaming

var stream = processor.Stream(input);
await foreach (var result in stream)
{
    Console.WriteLine(result);
}

Complex Branching

var handler = defaultHandler.BranchContext(
    // Premium tier
    ((req, ctx) => ctx.TenantId == "premium", premiumHandler),
    // Enterprise tier
    ((req, ctx) => ctx.TenantId == "enterprise", enterpriseHandler),
    // Debug mode
    ((req, ctx) => ctx.GetValue<bool>("IsDebug"), debugHandler)
    // Default handler used if no match
);

Custom Cache Key Generation

var cached = service
    .WithCacheContext(
        ctx => $"region:{ctx.GetValue<string>("Region")}:tenant:{ctx.TenantId}",
        input => input.Id.ToString());

๐Ÿ”ง Configuration

Context Configuration

// Set context programmatically
RunnableContext.Current.TenantId = "tenant-123";
RunnableContext.Current.UserId = "user-456";
RunnableContext.Current.SetValue("CustomKey", "CustomValue");

// Access context anywhere
var tenantId = RunnableContext.Current.TenantId;
var customValue = RunnableContext.Current.GetValue<string>("CustomKey");

Metrics & Monitoring

var metrics = new MetricsCollector();

var pipeline = service
    .WithMetrics(metrics)
    .TapContext((result, ctx) => 
        metrics.Record("processed", 1, new {
            TenantId = ctx.TenantId,
            CorrelationId = ctx.CorrelationId
        }));

๐Ÿงช Testing

[Test]
public async Task TestPipelineWithContext()
{
    // Arrange
    RunnableContext.Current.TenantId = "test-tenant";
    
    var pipeline = processor
        .FilterContext((data, ctx) => data.TenantId == ctx.TenantId)
        .MapContext((result, ctx) => new Response {
            Data = result,
            TenantId = ctx.TenantId
        });
    
    // Act
    var result = await pipeline.InvokeAsync(testData);
    
    // Assert
    Assert.Equal("test-tenant", result.TenantId);
}

๐Ÿ“– API Reference

Context Properties

Property Type Description
CorrelationId string Correlation ID for distributed tracing
TraceId string Trace ID (defaults to CorrelationId)
ParentSpanId string Parent span ID for nested traces
TenantId string Tenant identifier for multi-tenancy
UserId string User identifier for audit logging

Context Methods

Method Description
GetValue<T>(key) Get custom context value
SetValue(key, value) Set custom context value
Clear() Clear all context data
GetAllData() Get all context as dictionary

๐ŸŒŸ Why Runnable?

โœ… Type-Safe

Full IntelliSense support with generic types throughout

โœ… Composable

Build complex pipelines from simple, reusable components

โœ… Multi-Tenant Ready

Built-in tenant isolation and context propagation

โœ… Production-Ready

Retry logic, caching, timeouts, and circuit breakers included

โœ… Testable

Easy to mock and test individual pipeline stages

โœ… Performant

Async-first design with efficient caching and LRU eviction

๐Ÿค Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

๐Ÿ“„ License

This project is licensed under the MIT License - see the LICENSE file for details.

๐Ÿ™ Acknowledgments

Inspired by:

  • LangChain - The original LCEL concept
  • Functional programming principles
  • Railway-oriented programming

๐Ÿ“š More Examples

Check out the examples directory for more detailed examples:

  • Multi-tenant API processing
  • A/B testing implementations
  • Complex branching scenarios
  • Custom caching strategies
  • Distributed tracing setup

๐Ÿ’ฌ Support


Built with โค๏ธ for the .NET community

Product Compatible and additional computed target framework versions.
.NET net6.0 is compatible.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 is compatible.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • net10.0

    • No dependencies.
  • net6.0

    • No dependencies.
  • net8.0

    • No dependencies.
  • net9.0

    • No dependencies.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.0 85 1/30/2026