A powerful durable orchestration framework with our own API design!
Asynkron.DurableFunctions is an independent durable orchestration framework that runs on any .NET environment - on-premises, Docker, Kubernetes, or any cloud provider. No vendor lock-in, just pure orchestration power!
- β Lightning fast - No heavyweight runtime overhead
- β Multiple storage backends - In-memory, SQLite, or bring your own
- β Rich orchestration patterns - Powerful workflow capabilities
- β Easy debugging - Debug locally with standard .NET tooling
- β Lightweight - Minimal dependencies, maximum performance
Install the NuGet package:
dotnet add package Asynkron.DurableFunctions
using Asynkron.DurableFunctions;
using Microsoft.Extensions.Logging;
// Create runtime - completely independent!
var stateStore = new InMemoryStateStore();
using var loggerFactory = LoggerFactory.Create(builder => builder.AddConsole());
var runtime = new DurableFunctionRuntime(
stateStore,
loggerFactory.CreateLogger<DurableFunctionRuntime>(),
loggerFactory: loggerFactory);
// Register functions using our CallFunction API
runtime.RegisterFunction<string, string>("SayHello", async name =>
{
await Task.Delay(100); // Simulate some work
return $"Hello, {name}!";
});
// Register orchestrator - notice the CallFunction usage!
runtime.RegisterOrchestrator<string>("GreetingOrchestrator", async context =>
{
var name = context.GetInput<string>();
var greeting = await context.CallFunction<string>("SayHello", name);
return $"Orchestrator says: {greeting}";
});
// Trigger and run!
await runtime.TriggerAsyncObject("user123", "GreetingOrchestrator", "World");
await runtime.RunAndPollAsync(CancellationToken.None);
Output:
Orchestrator says: Hello, World!
Already using Azure Durable Functions? Great news! Asynkron.DurableFunctions includes an Azure compatibility adapter that makes migration incredibly simple.
β
Break free from vendor lock-in - Deploy anywhere: on-premises, Docker, Kubernetes, any cloud
β
Cost control - No per-execution billing, predictable infrastructure costs
β
Local development - Full functionality without Azure dependencies or emulators
β
Enhanced debugging - Use standard .NET debugging tools locally
β
Production flexibility - Scale and deploy on your own terms
Your existing Azure Durable Functions code works with minimal changes:
// Your EXISTING Azure code - works as-is!
public class OrderProcessingFunctions
{
[FunctionName("ProcessOrderOrchestrator")]
public async Task<string> ProcessOrder([DurableOrchestrationTrigger] IDurableOrchestrationContext context)
{
var order = context.GetInput<OrderRequest>();
// These calls work exactly the same!
var validated = await context.CallActivityAsync<OrderRequest>("ValidateOrder", order);
var charged = await context.CallActivityAsync<OrderRequest>("ChargePayment", validated);
var shipped = await context.CallActivityAsync<OrderRequest>("ShipOrder", charged);
return $"Order {order.Id} processed successfully!";
}
[FunctionName("ValidateOrder")]
public async Task<OrderRequest> ValidateOrder([DurableActivityTrigger] IDurableActivityContext context)
{
var order = context.GetInput<OrderRequest>();
// Your validation logic here
return order;
}
}
-
Install the adapter:
dotnet add package Asynkron.DurableFunctions.AzureAdapter
-
Replace the hosting runtime:
// Replace Azure Functions host with Asynkron runtime var stateStore = new SqliteStateStore("Data Source=app.db"); var runtime = new DurableFunctionRuntime(stateStore, logger); // Auto-register your existing functions - no code changes needed! runtime.RegisterAzureFunctionsFromType(typeof(OrderProcessingFunctions), new OrderProcessingFunctions()); // Run anywhere - Docker, Kubernetes, on-premises, any cloud! await runtime.RunAndPollAsync(cancellationToken);
-
Deploy anywhere you want! No more Azure lock-in.
β Keep (unchanged):
- All your
[FunctionName]
attributes IDurableOrchestrationContext
andIDurableActivityContext
interfacesCallActivityAsync
,WaitForExternalEvent
,CreateTimer
methods- Your business logic and workflow patterns
- Familiar debugging and development experience
π Changes (minimal):
- Replace Azure Functions runtime with Asynkron runtime
- Choose your own storage backend (SQLite, InMemory, or custom)
- Deploy using standard .NET hosting instead of Azure Functions
View complete Azure migration example β | Azure Adapter Documentation β
This library provides comprehensive support for all major durable orchestration patterns:
Orchestrations can wait for external events and resume when they arrive. This enables human interaction patterns and event-driven workflows.
// Orchestrator waiting for external event
runtime.RegisterOrchestrator<string>("ApprovalOrchestrator", async context =>
{
// Send approval request
await context.CallFunction("SendApprovalRequest", context.GetInput<string>());
// Wait for external approval event
var approved = await context.WaitForExternalEvent<bool>("ApprovalEvent");
if (approved)
{
await context.CallFunction("ProcessApproval", "Approved");
return "Request approved";
}
else
{
await context.CallFunction("ProcessRejection", "Rejected");
return "Request rejected";
}
});
// Later, raise the event from external system
await runtime.RaiseEventAsync("approval-123", "ApprovalEvent", true);
Each call to
WaitForExternalEvent
reserves its own slot. If an orchestrator waits for the same event multiple times, it must receive the same number ofRaiseEventAsync
calls. Events are persisted in FIFO order per name, and the runtime logs queue depth when deliveries pile up so you can detect backlogs.
Call other orchestrators as sub-orchestrations for complex workflow composition. The runtime automatically handles parent-child relationships.
// Child orchestrator
runtime.RegisterOrchestrator<string>("ProcessOrderOrchestrator", async context =>
{
var order = context.GetInput<string>();
await context.CallFunction("ValidateOrder", order);
await context.CallFunction("ChargePayment", order);
return "Order processed";
});
// Parent orchestrator calling sub-orchestrator
runtime.RegisterOrchestrator<string>("MainOrchestrator", async context =>
{
var mainOrder = context.GetInput<string>();
// Call sub-orchestrator explicitly
var result = await context.CallSubOrchestratorAsync<string>("ProcessOrderOrchestrator", mainOrder);
await context.CallFunction("SendNotification", result);
return "Workflow completed";
});
Combine external events with functions to create human approval workflows:
runtime.RegisterOrchestrator<string>("HumanApprovalOrchestrator", async context =>
{
var request = context.GetInput<string>();
// Send approval request to human
await context.CallFunction("SendApprovalEmail", request);
// Wait for human response (timeout after 24 hours)
using var cts = new CancellationTokenSource(TimeSpan.FromHours(24));
try
{
var approved = await context.WaitForExternalEvent<bool>("HumanApproval");
return approved ? "Approved by human" : "Rejected by human";
}
catch (OperationCanceledException)
{
return "Approval timed out";
}
});
Create long-running monitor patterns using durable timers:
runtime.RegisterOrchestrator<string>("MonitorOrchestrator", async context =>
{
var monitorConfig = context.GetInput<string>();
while (true) // Eternal loop
{
// Check system health
var status = await context.CallFunction<string>("CheckSystemHealth", monitorConfig);
if (status != "OK")
{
await context.CallFunction("SendAlert", status);
}
// Wait 5 minutes before next check
await context.CreateTimer(context.CurrentUtcDateTime.AddMinutes(5));
}
});
Our clean CallFunction API in action:
[FunctionName("ProcessOrderOrchestrator")]
public async Task<string> ProcessOrder([OrchestrationTrigger] IDurableOrchestrationContext context)
{
var order = context.GetInput<OrderRequest>();
// Sequential processing - each step waits for the previous
var validated = await context.CallFunction<OrderRequest>("ValidateOrder", order);
var charged = await context.CallFunction<OrderRequest>("ChargePayment", validated);
var shipped = await context.CallFunction<OrderRequest>("ShipOrder", charged);
var notified = await context.CallFunction<string>("NotifyCustomer", shipped);
return $"Order {order.Id} processed successfully! {notified}";
}
// Functions are just functions!
[FunctionName("ValidateOrder")]
public async Task<OrderRequest> ValidateOrder([ActivityTrigger] OrderRequest order)
{
Console.WriteLine($"Validating order {order.Id}...");
await Task.Delay(500); // Simulate validation
if (order.Amount <= 0) throw new ArgumentException("Invalid amount");
return order;
}
[FunctionName("ChargePayment")]
public async Task<OrderRequest> ChargePayment([ActivityTrigger] OrderRequest order)
{
Console.WriteLine($"Charging ${order.Amount} for order {order.Id}...");
await Task.Delay(1000); // Simulate payment processing
return order;
}
Process multiple things concurrently, then combine results:
[FunctionName("ParallelProcessingOrchestrator")]
public async Task<string> ProcessParallel([OrchestrationTrigger] IDurableOrchestrationContext context)
{
var inputs = new[] { "data1", "data2", "data3", "data4", "data5" };
// Fan-out: Start all functions in parallel
var tasks = inputs.Select(input =>
context.CallFunction<string>("ProcessData", input)
).ToArray();
// Fan-in: Wait for all to complete
var results = await Task.WhenAll(tasks);
return $"Processed {results.Length} items: {string.Join(", ", results)}";
}
[FunctionName("ProcessData")]
public async Task<string> ProcessData([ActivityTrigger] string data)
{
Console.WriteLine($"Processing {data}...");
await Task.Delay(Random.Shared.Next(500, 1500)); // Simulate variable work
return $"Processed-{data}";
}
Create workflows that wait for hours, days, or weeks:
[FunctionName("LongRunningProcess")]
public async Task<string> LongRunningProcess([OrchestrationTrigger] IDurableOrchestrationContext context)
{
var startTime = context.CurrentUtcDateTime;
// Send welcome email immediately
await context.CallFunction("SendWelcomeEmail", context.GetInput<string>());
// Wait 24 hours (orchestrator will hibernate and wake up automatically!)
var tomorrow = startTime.AddHours(24);
await context.CreateTimer(tomorrow);
// Send follow-up email after 24 hours
await context.CallFunction("SendFollowUpEmail", context.GetInput<string>());
// Wait a whole week! (Server can restart, no problem!)
var nextWeek = startTime.AddDays(7);
await context.CreateTimer(nextWeek);
// Send weekly newsletter
await context.CallFunction("SendWeeklyNewsletter", context.GetInput<string>());
return "Email sequence completed over 7 days!";
}
Wait for external events (like user approval):
[FunctionName("ApprovalWorkflow")]
public async Task<string> ApprovalWorkflow([OrchestrationTrigger] IDurableOrchestrationContext context)
{
var request = context.GetInput<ApprovalRequest>();
// Submit for approval
await context.CallFunction("SendApprovalRequest", request);
// Wait for external approval event (could be hours or days!)
var approvalResult = await context.WaitForExternalEvent<bool>("ApprovalEvent");
if (approvalResult)
{
await context.CallFunction("ProcessApprovedRequest", request);
return "Request approved and processed!";
}
else
{
await context.CallFunction("HandleRejection", request);
return "Request was rejected.";
}
}
// To trigger approval from external system:
// await runtime.RaiseEventAsync(instanceId, "ApprovalEvent", true);
Built-in resilience patterns:
[FunctionName("ResilientOrchestrator")]
public async Task<string> ResilientProcess([OrchestrationTrigger] IDurableOrchestrationContext context)
{
try
{
// This might fail, but will retry automatically
var result = await context.CallFunction<string>("UnreliableFunction", "test-data");
return $"Success: {result}";
}
catch (Exception ex)
{
// Handle failure after all retries exhausted
await context.CallFunction("LogError", ex.Message);
return "Failed after retries";
}
}
[FunctionName("UnreliableFunction")]
public async Task<string> UnreliableFunction([ActivityTrigger] string data)
{
// Simulate 70% failure rate
if (Random.Shared.NextDouble() < 0.7)
{
throw new InvalidOperationException("Simulated failure!");
}
return $"Successfully processed: {data}";
}
Use familiar function attributes:
// Standard function registration patterns
public class MyOrchestrations
{
[Function("EmailCampaignOrchestrator")]
public async Task<string> EmailCampaign([OrchestrationTrigger] IDurableOrchestrationContext context)
{
var campaign = context.GetInput<Campaign>();
foreach (var customer in campaign.Customers)
{
await context.CallFunction("SendPersonalizedEmail", customer);
}
return $"Sent {campaign.Customers.Count} emails!";
}
[Function("SendPersonalizedEmail")]
public async Task SendPersonalizedEmail([ActivityTrigger] Customer customer)
{
// Your email logic here
Console.WriteLine($"Sending email to {customer.Email}");
await Task.Delay(100);
}
}
// Auto-register all functions using reflection
runtime.ScanAndRegister(typeof(MyOrchestrations).Assembly);
Never lose state, even if your server restarts:
// Use SQLite for persistence (survives restarts!)
var connectionString = "Data Source=durable_functions.db";
using var stateStore = new SqliteStateStore(connectionString);
var runtime = new DurableFunctionRuntime(stateStore, logger, loggerFactory: loggerFactory);
// Your orchestrations will survive server restarts! π
Type-safe inputs and outputs:
public class OrderRequest
{
public string ProductName { get; set; } = "";
public int Quantity { get; set; }
public decimal Price { get; set; }
}
public class OrderResult
{
public string OrderId { get; set; } = "";
public string Status { get; set; } = "";
public DateTime ProcessedAt { get; set; }
}
// Strongly typed orchestrator
runtime.RegisterOrchestratorFunction<OrderRequest, OrderResult>("ProcessTypedOrder", async context =>
{
var order = context.GetInput<OrderRequest>(); // β
Type-safe!
// Grab a replay-safe logger (use GetLogger<MyCategory>() for typed categories)
var logger = context.GetLogger();
logger.LogInformation($"Processing order for {order.ProductName}");
return new OrderResult
{
OrderId = Guid.NewGuid().ToString(),
Status = "Completed",
ProcessedAt = DateTime.UtcNow
};
});
Asynkron.DurableFunctions is inspired by orchestration concepts from various sources, but this is our own independent project with our own API design.
- CallFunction is the core - Simple, clean function invocation
- No vendor dependency - Runs anywhere .NET runs
- Our design decisions - API designed for clarity and power
- Community-driven - Open to ideas and contributions
While the orchestration patterns are similar to other durable function frameworks, the API and implementation are completely independent.
- Order processing workflows
- Approval chains
- Document processing pipelines
- Customer onboarding flows
- ETL pipelines with error handling
- Batch processing with fan-out/fan-in
- Multi-step data transformations
- Report generation workflows
- Multi-system integration workflows
- API orchestration and aggregation
- Event-driven processing chains
- Saga pattern implementations
- Scheduled report generation
- Reminder and notification systems
- Delayed processing workflows
- Long-running business processes
using Asynkron.DurableFunctions;
using Microsoft.Extensions.Logging;
var stateStore = new InMemoryStateStore();
using var loggerFactory = LoggerFactory.Create(b => b.AddConsole());
var runtime = new DurableFunctionRuntime(
stateStore,
loggerFactory.CreateLogger<DurableFunctionRuntime>(),
loggerFactory: loggerFactory);
// Simple function
runtime.RegisterFunction<string, string>("Greet", async name => $"Hello {name}! π");
// Simple orchestrator using CallFunction
runtime.RegisterOrchestratorFunction<string, string>("HelloOrchestrator", async context =>
{
var name = context.GetInput<string>();
return await context.CallFunction<string>("Greet", name);
});
// Run it!
await runtime.TriggerAsync("test", "HelloOrchestrator", "World");
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
await runtime.RunAndPollAsync(cts.Token);
// Register a complete workflow using CallFunction
runtime.RegisterOrchestratorFunction<string, string>("DataPipelineOrchestrator", async context =>
{
var data = context.GetInput<string>();
// Step 1: Validate
var validated = await context.CallFunction<string>("ValidateData", data);
// Step 2: Transform
var transformed = await context.CallFunction<string>("TransformData", validated);
// Step 3: Store
var result = await context.CallFunction<string>("StoreData", transformed);
return $"Pipeline complete: {result}";
});
// Register functions
runtime.RegisterFunction<string, string>("ValidateData", async data =>
{
Console.WriteLine($"π Validating: {data}");
await Task.Delay(100);
return $"validated-{data}";
});
runtime.RegisterFunction<string, string>("TransformData", async data =>
{
Console.WriteLine($"π Transforming: {data}");
await Task.Delay(200);
return $"transformed-{data}";
});
runtime.RegisterFunction<string, string>("StoreData", async data =>
{
Console.WriteLine($"πΎ Storing: {data}");
await Task.Delay(150);
return $"stored-{data}";
});
runtime.RegisterOrchestratorFunction<string, string>("DelayedGreeting", async context =>
{
var name = context.GetInput<string>();
Console.WriteLine($"β° Setting timer for 5 seconds...");
var dueTime = context.CurrentUtcDateTime.AddSeconds(5);
await context.CreateTimer(dueTime);
Console.WriteLine($"π Timer fired! Greeting {name}");
return $"Hello {name} (after delay)!";
});
var stateStore = new InMemoryStateStore();
var stateStore = new SqliteStateStore("Data Source=app.db");
public class MyCustomStateStore : IStateStore
{
// Implement your storage logic (Redis, MongoDB, etc.)
}
using var loggerFactory = LoggerFactory.Create(builder =>
builder
.AddConsole()
.AddSerilog() // Or any logging provider
.SetMinimumLevel(LogLevel.Information)
);
var builder = WebApplication.CreateBuilder(args);
// Add durable functions as a service
builder.Services.AddSingleton<IStateStore>(sp =>
new SqliteStateStore(builder.Configuration.GetConnectionString("StateStore")));
builder.Services.AddSingleton<DurableFunctionRuntime>();
var app = builder.Build();
// Auto-start the runtime
var runtime = app.Services.GetRequiredService<DurableFunctionRuntime>();
_ = Task.Run(() => runtime.RunAndPollAsync(CancellationToken.None));
app.Run();
- Lightweight: Minimal overhead compared to Azure Functions runtime
- Fast startup: No cold start issues
- Horizontally scalable: Run multiple instances with shared storage
- Efficient storage: Optimized state serialization
- Automatic cleanup: Completed orchestrations are automatically cleaned up
π Ready to break free from Azure lock-in?
β Star this repo β’ π¦ Use it in production
Built with β€οΈ by the Asynkron team