SquirrelBox 3.2.1
dotnet add package SquirrelBox --version 3.2.1
NuGet\Install-Package SquirrelBox -Version 3.2.1
<PackageReference Include="SquirrelBox" Version="3.2.1" />
<PackageVersion Include="SquirrelBox" Version="3.2.1" />
<PackageReference Include="SquirrelBox" />
paket add SquirrelBox --version 3.2.1
#r "nuget: SquirrelBox, 3.2.1"
#:package SquirrelBox@3.2.1
#addin nuget:?package=SquirrelBox&version=3.2.1
#tool nuget:?package=SquirrelBox&version=3.2.1
SquirrelBox
SquirrelBox is the Elysium inbox/outbox toolkit for .NET services.
It protects incoming work from duplicate execution with the inbox pattern, persists outgoing work with the outbox pattern, and uses Mule durable actions for deferred execution. The core is transport-neutral: HTTP, messaging, Pigeon, custom transports, and dashboard diagnostics all use the same model.
Packages
dotnet add package SquirrelBox
dotnet add package SquirrelBox.InMemory
dotnet add package SquirrelBox.EntityFrameworkCore
dotnet add package SquirrelBox.AspNetCore
dotnet add package SquirrelBox.AspNetCore.Dashboard
dotnet add package SquirrelBox.Messaging
dotnet add package SquirrelBox.Messaging.Pigeon
dotnet add package SquirrelBox.Mule
Package reference example:
<PackageReference Include="SquirrelBox" Version="3.2.1" />
<PackageReference Include="SquirrelBox.AspNetCore" Version="3.2.1" />
<PackageReference Include="SquirrelBox.AspNetCore.Dashboard" Version="3.2.1" />
<PackageReference Include="SquirrelBox.EntityFrameworkCore" Version="3.2.1" />
<PackageReference Include="SquirrelBox.Mule" Version="3.2.1" />
Getting Started
Register core services, choose storage, and add Mule when you want durable deferred execution:
using Mule.InMemory;
using SquirrelBox;
using SquirrelBox.EntityFrameworkCore;
using SquirrelBox.Mule;
services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(connectionString));
services
.AddSquirrelBox(options =>
{
options.DefaultEntryLifetime = TimeSpan.FromHours(24);
options.AllowPayloadHashAsIdempotencyKey = true;
options.ScanAssemblyContaining<OrdersFingerprintProfile>();
options.AddInboxPolicy("payments", policy =>
{
policy.CompletedLock = InboxCompletedLockMode.Forever;
});
options.AddInboxPolicy("orders-deferred", policy =>
{
policy.ExecutionMode = InboxExecutionMode.Deferred;
policy.Deferred.Lane = "orders-deferred";
policy.Deferred.MaxAttempts = 3;
policy.Deferred.Delay = TimeSpan.FromSeconds(2);
policy.Deferred.Backoff = InboxRetryBackoff.Exponential;
policy.Deferred.InProgressTimeout = TimeSpan.FromMinutes(10);
});
})
.UseEntityFramework<AppDbContext>();
services.AddSquirrelBoxMule();
services.AddMule(mule => mule
.UseInMemory()
.AddActionsFromAssemblyContaining<SquirrelBoxOutboxMuleAction>());
SquirrelBox storage providers are configured from the builder returned by AddSquirrelBox.
For EF Core, call UseEntityFramework<TDbContext>() after registering the application
DbContext. SquirrelBox augments the registered DbContextOptions<TDbContext>
automatically; your application DbContext does not need SquirrelBox DbSet properties,
OnModelCreating changes, or model builder calls.
Use UseInMemory() instead of EF for tests, samples, and local development.
using SquirrelBox.InMemory;
services
.AddSquirrelBox()
.UseInMemory();
Storage providers are intentionally exposed through AddSquirrelBox() instead of standalone
IServiceCollection methods. If older code registered storage separately, move the provider
call into the SquirrelBox chain:
services
.AddSquirrelBox()
.UseEntityFramework<AppDbContext>();
Inbox
The inbox pattern identifies incoming work by:
Source + Operation + IdempotencyKey
Basic usage:
var open = await inbox.OpenOrContinueAsync(
InboxOpenRequest.For(
source: "http",
operation: "POST /orders",
idempotencyKey: "client-key-1",
payload: request),
cancellationToken);
if (!open.Accepted)
return;
try
{
await handler.Handle(request, cancellationToken);
await inbox.CompleteCurrentAsync(cancellationToken: cancellationToken);
}
catch (Exception ex)
{
await inbox.FailCurrentAsync(ex, cancellationToken);
throw;
}
Entries use ULID ids and move through Started, Completed, Failed, and Expired.
By default, idempotency is a time window. DefaultEntryLifetime is 24 hours unless you
override it. Expired entries and failed entries can be opened again, so a failed request
does not poison the same payload forever. Completed entries only block forever when the
entrypoint explicitly selects InboxCompletedLockMode.Forever.
Identity Metadata
SquirrelBox now tracks two identity layers for every inbox open attempt:
Operation identity: IdempotencyKey + CorrelationId
Attempt identity: AttemptId + TraceId
The idempotency key and correlation id are matched 1:1 for the protected operation. If the first request/message does not provide either value, SquirrelBox can compute the idempotency key from the semantic payload hash and generate the correlation id. Later duplicates that produce the same idempotency key return the original correlation id, while each duplicate open attempt receives a new attempt id and trace id.
var identity = inbox.LastContext?.Identity;
var operationKey = identity?.Operation.IdempotencyKey.Value;
var correlationId = identity?.Operation.CorrelationId.Value;
var attemptId = identity?.Attempt.AttemptId.Value;
var traceId = identity?.Attempt.TraceId.Value;
Services in the current request/message scope can also inject ISquirrelBoxIdentityAccessor
to read the current accepted identity. The default factories are replaceable:
services.AddSingleton<ICorrelationIdFactory, MyCorrelationIdFactory>();
services.AddSingleton<ITraceIdFactory, MyTraceIdFactory>();
services.AddSingleton<IAttemptIdFactory, MyAttemptIdFactory>();
services.AddSquirrelBox();
Register custom factories before AddSquirrelBox() or replace the service descriptor explicitly.
Declared Operations
Declared operations are the durable-safe path for switching HTTP or other synchronous entry points between inline and deferred execution.
public sealed class CreateOrderOperation
: SquirrelBoxOperation<CreateOrderRequest, OrderCreated>
{
protected override async ValueTask<OrderCreated> ExecuteAsync(
CreateOrderRequest request,
SquirrelBoxOperationContext context,
CancellationToken cancellationToken)
{
var service = context.Services.GetRequiredService<IOrderService>();
return await service.CreateAsync(request, cancellationToken);
}
}
Invoke from an endpoint:
var result = await operations
.ExecuteAsync<CreateOrderOperation, CreateOrderRequest, OrderCreated>(
request,
cancellationToken);
if (result.Deferred)
return Results.Accepted($"/inbox/{result.Context.Entry.Id}");
if (result.Executed)
return Results.Created($"/orders/{result.Result.Id}", result.Result);
return Results.Conflict(result.Decision);
When execution is deferred, SquirrelBox stores the inbox entry first, schedules a Mule durable action, and completes/fails the inbox later from a worker scope. Deferred retry behavior is part of the selected inbox policy:
options.AddInboxPolicy("orders-deferred", policy =>
{
policy.ExecutionMode = InboxExecutionMode.Deferred;
policy.Deferred.Lane = "orders-deferred";
policy.Deferred.MaxAttempts = 5;
policy.Deferred.Delay = TimeSpan.FromSeconds(5);
policy.Deferred.MaxDelay = TimeSpan.FromMinutes(1);
policy.Deferred.Backoff = InboxRetryBackoff.Exponential;
policy.Deferred.JitterRatio = 0.10;
policy.Deferred.InProgressTimeout = TimeSpan.FromMinutes(15);
});
While Mule still has attempts available, SquirrelBox marks the inbox entry as Retrying.
Only the terminal Mule failure marks the entry as Failed. Normal EntryLifetime controls
the duplicate window for terminal entries; active entries stay in progress until they
complete/fail or pass the configured InProgressTimeout.
The sample project includes deferred HTTP endpoints for active duplicate blocking,
transient retry completion, terminal retry failure/reopen, and Pigeon deferred consumption.
It also demonstrates HTTP payload windows: expired completed entries can execute again,
while InboxCompletedLockMode.Forever replays the original completed response instead of
running the operation again.
Outbox
The outbox pattern persists outgoing work before it is delivered by a transport.
var envelope = await outbox.EnqueueAsync(new OutboxEnqueueRequest
{
Transport = "pigeon",
Operation = "orders.created",
Destination = "orders",
Payload = new OrderCreated(orderId),
CorrelationId = correlationId
}, cancellationToken);
SquirrelBox stores an OutboxEnvelope with:
Ulid Id
Transport
Operation
Destination
PayloadType
Payload
Headers
Metadata
CorrelationId
Status
Mule later executes squirrelbox.outbox.publish.v1, loads the envelope by ULID, resolves the matching IOutboxTransportPublisher, and marks the envelope as Published or Failed.
Custom publisher:
public sealed class MyPublisher : IOutboxTransportPublisher
{
public string Transport => "my-transport";
public async ValueTask<OutboxPublishResult> PublishAsync(
OutboxEnvelope envelope,
CancellationToken cancellationToken = default)
{
await client.SendAsync(envelope.Payload, cancellationToken);
return OutboxPublishResult.Success;
}
}
Profiles
Inbox fingerprints and outbox profiles are discovered from scanned assemblies.
public sealed class OrdersFingerprintProfile : InboxFingerprintProfile
{
public override void Configure(InboxFingerprintProfileBuilder builder)
{
builder.For<CreateOrderRequest>()
.Use(request => new
{
request.CustomerId,
request.ExternalOrderId,
request.Amount
});
}
}
Outbox profiles can customize how enqueue requests become durable envelopes without adding noisy fluent configuration.
ASP.NET Core
Add HTTP idempotency:
using SquirrelBox.AspNetCore;
services.AddSquirrelBoxAspNetCore(options =>
{
options.RequestHeaderNames.Clear();
options.RequestHeaderNames.Add("Idempotency-Key");
options.ResponseHeaderName = "Idempotency-Key";
options.CaptureCompletedResponses = true;
options.ReplayCompletedResponses = true;
});
app.UseSquirrelBox();
UseSquirrelBox() is not a global idempotency switch. A request is protected only when the
endpoint explicitly opens SquirrelBox, for example with [SquirrelBoxPayload],
.WithSquirrelBoxPayload(), or HttpContext.OpenSquirrelBoxAsync(...). Sending an
Idempotency-Key header to an unmarked endpoint does not activate inbox protection.
HTTP responses include the effective idempotency key, correlation id, attempt id, and trace id.
When the request uses a configured alternate header such as X-Idempotency-Key or
X-Correlation-Id, SquirrelBox propagates the same header name back. Computed keys use the
configured default response/request header name.
For MVC controllers, add [SquirrelBoxPayload] to the action that receives the bound DTO:
[HttpPost("orders")]
[SquirrelBoxPayload]
public async Task<ActionResult> Create(CreateOrderRequest request, CancellationToken cancellationToken)
{
var result = await operations.ExecuteAsync<CreateOrderOperation, CreateOrderRequest, OrderSnapshot>(
request,
cancellationToken);
return result.Executed
? Created($"/orders/{result.Result.Id}", result.Result)
: Conflict(result.Decision);
}
The attribute is method-only and reads the request action argument by default. Use
[SquirrelBoxPayload("payload")] when the DTO parameter has a different name. The same
attribute can select a named policy, entrypoint TTL, or completed-lock behavior:
[HttpPost("payments")]
[SquirrelBoxPayload(TtlSeconds = 86_400, CompletedLock = InboxCompletedLockMode.Forever)]
public async Task<ActionResult> CreatePayment(CreatePaymentRequest request)
{
// ...
}
MVC actions can also declare deferred entrypoint settings directly:
[HttpPost("orders/deferred")]
[SquirrelBoxPayload(
Policy = "orders-deferred",
DeferExecution = true,
DeferredLane = "orders-deferred",
RetryMaxAttempts = 3,
RetryDelaySeconds = 2,
RetryBackoff = InboxRetryBackoff.Exponential,
InProgressTimeoutSeconds = 600)]
public async Task<ActionResult> CreateDeferred(CreateOrderRequest request)
{
var result = await operations.ExecuteAsync<CreateOrderOperation, CreateOrderRequest, OrderSnapshot>(
request,
HttpContext.RequestAborted);
return result.Deferred
? Accepted($"/inbox/{result.Context.Entry.Id}", result.Context.Entry.Id)
: Created($"/orders/{result.Result.Id}", result.Result);
}
For Minimal APIs, attach the endpoint filter:
app.MapPost("/orders/computed-key", async (
CreateOrderRequest request,
ISquirrelBoxOperationService operations,
CancellationToken cancellationToken) =>
{
var result = await operations.ExecuteAsync<CreateOrderOperation, CreateOrderRequest, OrderSnapshot>(
request,
cancellationToken);
return result.Executed
? Results.Created($"/orders/{result.Result.Id}", result.Result)
: Results.Conflict(result.Decision);
})
.WithSquirrelBoxPayload();
Minimal APIs can configure the same policy data inline:
app.MapPost("/orders/window", HandleOrder)
.WithSquirrelBoxPayload(options =>
{
options.EntryLifetime = TimeSpan.FromMinutes(30);
});
app.MapPost("/payments", HandlePayment)
.WithSquirrelBoxPayload(options =>
{
options.PolicyName = "payments";
options.CompletedLock = InboxCompletedLockMode.Forever;
});
app.MapPost("/orders/deferred", HandleDeferredOrder)
.WithSquirrelBoxPayload(options =>
{
options.PolicyName = "orders-deferred";
options.ExecutionMode = InboxExecutionMode.Deferred;
options.Deferred.Lane = "orders-deferred";
options.Deferred.MaxAttempts = 3;
options.Deferred.Delay = TimeSpan.FromSeconds(2);
options.Deferred.InProgressTimeout = TimeSpan.FromMinutes(10);
});
Payload-aware endpoints let UseSquirrelBox() defer the open step until after model binding.
The filter opens or continues the inbox with the bound DTO, verifies that repeated explicit
keys keep the same payload hash, and reuses the middleware response behavior for replay and
conflict responses.
Dashboard
Add the event-driven dashboard:
using SquirrelBox.AspNetCore.Dashboard;
services.AddSquirrelBoxDashboard(options =>
{
options.Authentication.RootUser.Username = "admin";
options.Authentication.RootUser.Password = "<from-secret-store>";
});
app.MapSquirrelBoxDashboard("/squirrelbox");
The dashboard:
- Loads initial Inbox/Outbox history from storage.
- Receives live events through Server-Sent Events.
- Does not poll the database.
- Shows Inbox, Outbox, Deferred Work, and live Events.
- Uses the SquirrelBox logo palette.
- Is closed by default: root user, ASP.NET Core auth, or custom auth must be configured.
ASP.NET Core auth mode:
services.AddSquirrelBoxDashboard(options =>
{
options.Authentication.Mode = SquirrelBoxDashboardAuthenticationMode.AspNetCoreAuthentication;
});
app.MapSquirrelBoxDashboard("/squirrelbox")
.RequireAuthorization("SquirrelBoxDashboard");
Custom auth mode:
services.AddSingleton<ISquirrelBoxDashboardAuthenticator, MyDashboardAuthenticator>();
services.AddSquirrelBoxDashboard(options =>
{
options.Authentication.Mode = SquirrelBoxDashboardAuthenticationMode.Custom;
});
Messaging
Use SquirrelBox.Messaging when building a transport adapter or orchestration layer. Messaging
inbox is explicit: define policy profiles for the topics, versions, subscriptions, or operations
that should be protected.
services.AddSquirrelBoxMessaging(options =>
{
options.ScanAssemblyContaining<OrdersMessageInboxProfile>();
});
public sealed class OrdersMessageInboxProfile : InboxMessagePolicyProfile
{
public override void Configure(InboxMessagePolicyProfileBuilder builder)
{
builder
.ForTopic("orders")
.Version("1.0.0")
.Subscription("billing")
.WithEntryLifetime(TimeSpan.FromHours(12));
builder
.ForTopic("payments")
.WithCompletedLock(InboxCompletedLockMode.Forever);
}
}
Then open from the adapter or orchestration layer:
var result = await messages.OpenAsync(new InboxMessageContext
{
Transport = "rabbitmq",
Topic = "orders",
Version = "1.0.0",
Subscription = "billing",
Operation = "created",
MessageId = messageId,
Payload = payload,
Metadata = metadata
});
if (!result.Enabled)
{
await consumer.Handle(payload, cancellationToken);
return;
}
if (result.ShouldExecute)
{
await consumer.Handle(payload, cancellationToken);
await inbox.CompleteCurrentAsync(cancellationToken: cancellationToken);
}
messages.AttachEffectiveKey(replyMetadata);
The default operation shape is:
topic:version/subscription/operation
AttachEffectiveKey writes the complete SquirrelBox identity as one structured metadata section:
{
"SquirrelBoxMetadata": {
"idempotencyKey": "orders:...",
"correlationId": "01K...",
"traceId": "01K...",
"attemptId": "01K..."
}
}
Messaging adapters read only the SquirrelBoxMetadata section for SquirrelBox identity propagation.
Broker metadata such as message-id can still be used as a fallback idempotency source when configured,
and payload hashing remains the final fallback when enabled.
Adapters can use ISquirrelBoxMessageMetadataEnricher directly when they need to attach the
current identity to replies, orchestration metadata, or outgoing messages without depending on
Pigeon.
Pigeon
SquirrelBox.Messaging.Pigeon targets Pigeon 4.0.0 and integrates with consume and publish interceptors.
services.AddSquirrelBoxMessaging(options =>
{
options.ScanAssemblyContaining<OrdersMessageInboxProfile>();
});
services.AddSquirrelBoxPigeon(options =>
{
options.Transport = "pigeon";
options.EnableOutbox = true;
});
Consume:
- Decision interceptor opens the inbox before the consumer handler only when a messaging inbox profile matches the topic/version/subscription/operation.
- Execution interceptor completes or fails the inbox after the handler.
- Deferred replay uses
IPigeonConsumerInvoker.
Messaging policies can select core policies or configure deferred retry settings directly:
public sealed class OrdersMessageInboxProfile : InboxMessagePolicyProfile
{
public override void Configure(InboxMessagePolicyProfileBuilder builder)
{
builder.Match(topic: "orders.deferred", version: "1.0.0", subscription: "billing")
.UseCorePolicy("orders-deferred");
builder.ForTopic("orders.priority")
.DeferExecution()
.WithDeferred(deferred =>
{
deferred.Lane = "orders-priority";
deferred.MaxAttempts = 5;
deferred.Delay = TimeSpan.FromSeconds(1);
deferred.Backoff = InboxRetryBackoff.Linear;
deferred.InProgressTimeout = TimeSpan.FromMinutes(5);
});
}
}
Publish:
- Publish decision interceptor persists Pigeon's prepared
PigeonPublishEnvelopein SquirrelBox Outbox. - Pigeon publish is skipped inline after the envelope is durable.
- Outgoing Pigeon envelopes are enriched with a single
SquirrelBoxMetadatasection when a current inbox identity exists. - Mule later publishes through
IPigeonPublisherInvokerwithout rerunning producer interceptors, publish decision interceptors, or Pigeon's internal outbox logic. - Normal and raw publish flows are supported.
Mule
Register SquirrelBox actions with Mule:
services.AddSquirrelBoxMule();
services.AddMule(mule => mule
.UseInMemory()
.AddActionsFromAssemblyContaining<SquirrelBoxOutboxMuleAction>()
.AddActionsFromAssemblyContaining<SquirrelBoxPigeonMuleAction>());
SquirrelBox attaches durable metadata such as squirrelbox-inbox-id, squirrelbox-outbox-id, retry policy metadata, and the structured SquirrelBoxMetadata section. When an inbox policy configures deferred retries, AddSquirrelBoxMule() creates or updates the Mule lane retry policy automatically. User Mule configuration can still override lane behavior by configuring the same lane after SquirrelBox registration.
Sample
Run the sample app:
dotnet run --project samples/SquirrelBox.Sample/SquirrelBox.Sample.csproj --urls http://127.0.0.1:5188
Try:
curl -i -X POST http://127.0.0.1:5188/orders/inline \
-H "Content-Type: application/json" \
-H "Idempotency-Key: inline-key-1" \
-d "{\"customerId\":\"cust-1\",\"externalOrderId\":\"inline-1\",\"amount\":42.5}"
curl -i -X POST http://127.0.0.1:5188/orders/no-inbox \
-H "Content-Type: application/json" \
-H "Idempotency-Key: ignored-by-unmarked-endpoint" \
-d "{\"customerId\":\"cust-1\",\"externalOrderId\":\"direct-1\",\"amount\":10.0}"
curl -i -X POST http://127.0.0.1:5188/orders/window \
-H "Content-Type: application/json" \
-d "{\"customerId\":\"cust-2\",\"externalOrderId\":\"window-1\",\"amount\":25.0}"
curl -i -X POST http://127.0.0.1:5188/orders/deferred \
-H "Content-Type: application/json" \
-H "Idempotency-Key: deferred-key-1" \
-d "{\"customerId\":\"cust-3\",\"externalOrderId\":\"deferred-1\",\"amount\":99.99}"
curl -i -X POST http://127.0.0.1:5188/pigeon/inline \
-H "Content-Type: application/json" \
-d "{\"orderId\":\"pigeon-inline-1\",\"customerId\":\"cust-4\",\"amount\":12.50}"
curl -i -X POST http://127.0.0.1:5188/outbox/direct \
-H "Content-Type: application/json" \
-d "{\"orderId\":\"audit-1\",\"reason\":\"manual-check\"}"
Open the dashboard at http://127.0.0.1:5188/squirrelbox with admin / secret.
Testing
The test suite covers:
- Core inbox lifecycle, policies, fingerprints, operation execution, and outbox publication.
- In-memory inbox/outbox storage.
- ASP.NET Core idempotency and dashboard auth/state.
- Messaging and Pigeon consume/publish adapters.
- Mule deferred inbox and outbox execution.
- EF Core SQL Server e2e tests using Docker.
Run everything:
dotnet test SquirrelBox.slnx -c Debug
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | 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. |
-
net10.0
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 8.0.2)
- Microsoft.Extensions.Options (>= 8.0.2)
- Ulid (>= 1.4.1)
-
net8.0
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 8.0.2)
- Microsoft.Extensions.Options (>= 8.0.2)
- Ulid (>= 1.4.1)
-
net9.0
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 8.0.2)
- Microsoft.Extensions.Options (>= 8.0.2)
- Ulid (>= 1.4.1)
NuGet packages (8)
Showing the top 5 NuGet packages that depend on SquirrelBox:
| Package | Downloads |
|---|---|
|
SquirrelBox.Mule
Mule durable action adapter for deferring SquirrelBox inbox execution. |
|
|
SquirrelBox.Messaging
Transport-neutral messaging adapter for SquirrelBox consumer idempotency. |
|
|
SquirrelBox.EntityFrameworkCore
Entity Framework Core storage provider for SquirrelBox durable inbox and outbox records. |
|
|
SquirrelBox.InMemory
In-memory inbox store for SquirrelBox tests, samples, and lightweight local idempotency behavior. |
|
|
SquirrelBox.AspNetCore
ASP.NET Core adapter for SquirrelBox HTTP idempotency. |
GitHub repositories
This package is not used by any popular GitHub repositories.