Skip to main content
Version: v1.5.2 (latest)

Outbox Components

To use the outbox channel, you need to provide three components that adapt the library to your persistence technology:

  1. Outbox Message Entity — The database row that stores the event
  2. Message Factory — Creates outbox records from CloudEvent instances
  3. Message Store — Repository for persisting and querying outbox records

Entity Framework Core users: If you're using the Hermodr.Publisher.Outbox.EntityFramework package, you can skip most of this — the ready-made DbOutboxMessage entity and EntityOutboxMessageRepository handle everything. See EF Core Integration for the recommended approach.


1. Outbox Message Entity​

Your entity class must implement IOutboxMessage. It acts as the database row that the relay service reads and the channel writes.

Required Properties​

PropertyTypePurpose
EventCloudEventThe event payload to deliver
StatusOutboxMessageStatusTracks delivery lifecycle
ErrorMessagestring?Last failure reason
RetryCountintNumber of delivery attempts
NextRetryAtDateTimeOffset?Earliest time for next retry

Minimal Entity Example​

using CloudNative.CloudEvents;
using Hermodr.Publisher.Outbox;

public class OrderOutboxMessage : IOutboxMessage
{
public string Id { get; set; } = Guid.NewGuid().ToString("N");

// IOutboxMessage — the CloudEvent payload
public CloudEvent Event { get; set; } = default!;

public OutboxMessageStatus Status { get; set; } = OutboxMessageStatus.Pending;
public string? ErrorMessage { get; set; }
public int RetryCount { get; set; }
public DateTimeOffset? NextRetryAt { get; set; }
}

Important: Direct IOutboxMessage implementation is only suited for custom persistence strategies (non-EF backends such as MongoDB, Dapper, etc.). For EF Core, use DbOutboxMessage from the Entity Framework package.


2. Message Factory​

IOutboxMessageFactory<TMessage> converts a CloudEvent into a new outbox entity instance. The factory should be stateless and allocation-light.

Interface​

public interface IOutboxMessageFactory<TMessage>
{
TMessage Create(CloudEvent cloudEvent, OutboxPublishOptions? options = null);
}

Example Implementation​

public class OrderOutboxMessageFactory : IOutboxMessageFactory<OrderOutboxMessage>
{
public OrderOutboxMessage Create(CloudEvent cloudEvent, OutboxPublishOptions? options = null)
=> new()
{
Event = cloudEvent,
Status = OutboxMessageStatus.Pending
};
}

The options parameter carries any per-call publish options (channel name, content-type overrides) in case you need to store them alongside the entity.


3. Message Store​

IOutboxMessageStore<TMessage> provides persistence operations for outbox messages.

Interface​

public interface IOutboxMessageStore<TMessage>
{
Task AddAsync(TMessage message, CancellationToken cancellationToken = default);
Task<OutboxMessageStatus> GetStatusAsync(TMessage message, CancellationToken cancellationToken = default);
Task<IReadOnlyList<TMessage>> GetPendingMessagesAsync(int? limit = null, CancellationToken cancellationToken = default);
Task SetSendingAsync(TMessage message, CancellationToken cancellationToken = default);
Task SetDeliveredAsync(TMessage message, CancellationToken cancellationToken = default);
Task SetDeferredAsync(TMessage message, DateTimeOffset scheduledAt, CancellationToken cancellationToken = default);
Task SetRetryAsync(TMessage message, Exception error, DateTimeOffset nextRetryAt, CancellationToken cancellationToken = default);
Task SetFailedAsync(TMessage message, Exception error, CancellationToken cancellationToken = default);
}

Method Reference​

MethodWhen CalledWhat to Do
AddAsyncOn publishPersist a new Pending outbox record
GetStatusAsyncOn-demand status checkReturn current status
GetPendingMessagesAsyncEvery relay tickReturn Pending records ready for delivery
SetSendingAsyncBefore delivery attemptSet Status = Sending to claim the record
SetDeliveredAsyncAfter successful deliverySet Status = Delivered
SetDeferredAsyncAfter initial deferralKeep Status = Pending, set NextRetryAt
SetRetryAsyncAfter transient failureSet Status = Pending, increment RetryCount, set NextRetryAt
SetFailedAsyncWhen retry limit exceededSet Status = Failed

Implementation Example (Simplified)​

public class MyOutboxMessageStore : IOutboxMessageStore<OrderOutboxMessage>
{
private readonly MyDbContext _db;

public MyOutboxMessageStore(MyDbContext db) => _db = db;

public async Task AddAsync(OrderOutboxMessage message, CancellationToken ct = default)
{
// Don't call SaveChangesAsync here - let the caller commit the unit of work
_db.OutboxMessages.Add(message);
}

public async Task<IReadOnlyList<OrderOutboxMessage>> GetPendingMessagesAsync(int? limit = null, CancellationToken ct = default)
{
var now = DateTimeOffset.UtcNow;
var query = _db.OutboxMessages
.Where(m => m.Status == OutboxMessageStatus.Pending
&& (m.NextRetryAt == null || m.NextRetryAt <= now))
.OrderBy(m => m.CreatedAt);

if (limit.HasValue)
query = query.Take(limit.Value);

return await query.ToListAsync(ct);
}

public async Task SetDeliveredAsync(OrderOutboxMessage message, CancellationToken ct = default)
{
message.Status = OutboxMessageStatus.Delivered;
message.LastStatusAt = DateTimeOffset.UtcNow;
await _db.SaveChangesAsync(ct);
}

// ... implement other methods similarly
}

Atomicity Tip: The AddAsync method is called from within the same DbContext scope as the domain entity write. For EF Core, you should not call SaveChangesAsync inside it — let the caller flush the unit of work. The state-transition methods (SetSendingAsync, etc.) are called by the relay service outside of a business transaction, so they should persist immediately.


Entity Framework Core Integration​

For EF Core users, the Hermodr.Publisher.Outbox.EntityFramework package eliminates all this boilerplate:

  • DbOutboxMessage — Complete, ready-to-use entity with EF mappings
  • EntityOutboxMessageRepository<TMessage> — Ready-made repository
  • WithEntityFramework() — Wires everything in one call

See EF Core Integration for the recommended approach.