Cleipnir.NET
Surviving crashes in plain C#-code
Install / Use
/learn @stidsborg/Cleipnir.NETREADME
Cleipnir.NET
Cleipnir.NET is a powerful durable execution framework for .NET — ensuring your code always completes correctly, even after crashes or restarts.
- 💡Crash-safe execution - plain C# code automatically resumes safely after failures or redeployments.
- ⏸️ Wait for external events - directly inside your code (without taking up resources)
- ⏰ Suspend execution - for seconds, minutes, or weeks — and continue seamlessly.
- ☁️ Cloud-agnostic - runs anywhere, only needs a database.
- ⚡ High performance - significantly faster than cloud-based workflow orchestrators.
- 📈 Scales horizontally - with replicas for high availability.
- 🔌 Built for ASP.NET and .NET Generic Host - easy to integrate.
- 📨 Compatible with all message brokers and service buses.
- 🧩 Eliminates complex patterns - like the Saga and Outbox/Inbox.
- 🗓️ A powerful job-scheduler alternative - to traditional job schedulers (Hangfire, Quartz, etc.).
Abstractions
The following three primitives form the foundation of Cleipnir.NET — together they allow you to express reliable business workflows as plain C# code that cannot fail.
Capture
Ensures that non-deterministic or side-effectful code (e.g., GUID generation, HTTP calls) produces the same result after a crash or restart.
var transactionId = await Capture("TransactionId", () => Guid.NewGuid());
//or simply
var transactionId = await Capture(Guid.NewGuid);
//can also be used for external calls with automatic retry
await Capture(() => httpClient.PostAsync("https://someurl.com", content), RetryPolicy.Default);
Messages
Wait for retrieval of external message - without consuming resources:
var fundsReserved = await Message<FundsReserved>(waitFor: TimeSpan.FromMinutes(5));
Suspension
Suspends execution for a given duration (without taking up in-memory resources) - after which it will resume automatically from the same point.
await Delay(TimeSpan.FromMinutes(5));
Examples
Message-brokered (source code):
public class OrderFlow(IBus bus) : Flow<Order>
{
public override async Task Run(Order order)
{
var transactionId = await Capture(Guid.NewGuid); //generated transaction id is fixed after this statement
await PublishReserveFunds(order, transactionId);
await Message<FundsReserved>(); //execution is suspended until a funds reserved message is received
await PublishShipProducts(order);
var trackAndTraceNumber = (await Message<ProductsShipped>()).TrackAndTraceNumber;
await PublishCaptureFunds(order, transactionId);
await Message<FundsCaptured>();
await PublishSendOrderConfirmationEmail(order, trackAndTraceNumber);
await Message<OrderConfirmationEmailSent>();
}
private Task PublishReserveFunds(Order order, Guid transactionId)
=> Capture(async () => await bus.Publish(new ReserveFunds(order.OrderId, order.TotalPrice, transactionId, order.CustomerId)));
}
RPC (source code):
public class OrderFlow(
IPaymentProviderClient paymentProviderClient,
IEmailClient emailClient,
ILogisticsClient logisticsClient
) : Flow<Order>
{
public override async Task Run(Order order)
{
var transactionId = await Capture(Guid.NewGuid); //generated transaction id is fixed after this statement
await Capture(() => paymentProviderClient.Reserve(order.CustomerId, transactionId, order.TotalPrice));
var trackAndTrace = await Capture(
() => logisticsClient.ShipProducts(order.CustomerId, order.ProductIds),
ResiliencyLevel.AtMostOnce
); //capture may also have at-most-once invocation semantics
await Capture(() => paymentProviderClient.Capture(transactionId));
await Capture(() => emailClient.SendOrderConfirmation(order.CustomerId, trackAndTrace, order.ProductIds));
}
}
What is durable execution?
Durable execution is an emerging paradigm for simplifying the implementation of code which can safely resume execution after a process crash or restart (i.e. after a production deployment). It allows the developer to implement such code using ordinary C#-code with loops, conditionals and so on.
Furthermore, durable execution allows suspending code execution for an arbitrary amount of time - thereby saving process resources.
Essentially, durable execution works by saving state at explicitly defined points during the invocation of code, thereby allowing the framework to skip over previously executed parts of the code when/if the code is re-executed. This occurs both after a crash and suspension.
Why durable execution?
Currently, implementing resilient business flows either entails (1) sagas (i.e. MassTransit, NServiceBus) or (2) job-schedulers (i.e. HangFire).
Both approaches have a unique set of challenges:
- Saga - becomes difficult to implement for real-world scenarios as they are either realized by declaratively constructing a state-machine or implementing a distinct message handler per message type.
- Job-scheduler - requires one to implement idempotent code by hand (in case of failure). Moreover, it cannot be integrated with message-brokers and does not support programmatically suspending a job in the middle of its execution.
Getting Started
To get started simply perform the following three steps in an ASP.NET or generic-hosted service (sample repo):
Firstly, install the Cleipnir.Flows nuget package (using either Postgres, SqlServer or MariaDB as persistence layer). I.e.
Install-Package Cleipnir.Flows.PostgresSql
Secondly, add the following to the setup in Program.cs (source code):
builder.Services.AddFlows(c => c
.UsePostgresStore(connectionString)
.RegisterFlows<OrderFlow, Order>()
);
RPC Flows
Finally, implement your flow (source code):
public class OrderFlow(
IPaymentProviderClient paymentProviderClient,
IEmailClient emailClient,
ILogisticsClient logisticsClient
) : Flow<Order>
{
public override async Task Run(Order order)
{
var transactionId = await Capture(Guid.NewGuid);
await Capture(() => paymentProviderClient.Reserve(order.CustomerId, transactionId, order.TotalPrice));
var trackAndTrace = await Capture(
() => logisticsClient.ShipProducts(order.CustomerId, order.ProductIds),
ResiliencyLevel.AtMostOnce
);
await Capture(() => paymentProviderClient.Capture(transactionId));
await Capture(() => emailClient.SendOrderConfirmation(order.CustomerId, trackAndTrace, order.ProductIds));
}
}
Message-brokered Flows
Or, if the flow is using a message-broker (source code):
public class OrderFlow(IBus bus) : Flow<Order>
{
public override async Task Run(Order order)
{
var transactionId = await Capture(Guid.NewGuid);
await PublishReserveFunds(order, transactionId);
await Message<FundsReserved>();
await PublishShipProducts(order);
var trackAndTraceNumber = (await Message<ProductsShipped>()).TrackAndTraceNumber;
await PublishCaptureFunds(order, transactionId);
await Message<FundsCaptured>();
await PublishSendOrderConfirmationEmail(order, trackAndTraceNumber);
await Message<OrderConfirmationEmailSent>();
}
The implemented flow can then be started using the corresponding source generated Flows-type (source code):
[ApiController]
[Route("[controller]")]
public class OrderController(Flows<OrderFlow, Order> orderFlows) : ControllerBase
{
[HttpPost]
public async Task Post(Order order) => await orderFlows.Run(order.OrderId, order);
}
Media
Discord
Get live help at the Discord channel:
Service Bus Integrations
It is simple to use Cleipnir with all the popular service bus frameworks. In order to do simply implement an event handler - which forwards received events - for each flow type:


