SkillAgentSearch skills...

Cleipnir.NET

Surviving crashes in plain C#-code

Install / Use

/learn @stidsborg/Cleipnir.NET

README

.NET NuGet NuGet alt Join the conversation

<p align="center"> <img src="https://github.com/stidsborg/Cleipnir.NET/blob/main/Docs/cleipnir.png" alt="logo" /> <br> Surviving <strong>crashes/restarts</strong> in <strong>plain</strong> C#-code <br> </p>

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

Video link      Video link

Discord

Get live help at the Discord channel:

alt Join the conversation

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:

MassTrans

View on GitHub
GitHub Stars140
CategoryDevelopment
Updated17d ago
Forks5

Languages

C#

Security Score

100/100

Audited on Mar 15, 2026

No findings