TCIS.EventBus 1.0.0-rc.18

This is a prerelease version of TCIS.EventBus.
There is a newer prerelease version of this package available.
See the version list below for details.
dotnet add package TCIS.EventBus --version 1.0.0-rc.18
                    
NuGet\Install-Package TCIS.EventBus -Version 1.0.0-rc.18
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="TCIS.EventBus" Version="1.0.0-rc.18" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="TCIS.EventBus" Version="1.0.0-rc.18" />
                    
Directory.Packages.props
<PackageReference Include="TCIS.EventBus" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add TCIS.EventBus --version 1.0.0-rc.18
                    
#r "nuget: TCIS.EventBus, 1.0.0-rc.18"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package TCIS.EventBus@1.0.0-rc.18
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=TCIS.EventBus&version=1.0.0-rc.18&prerelease
                    
Install as a Cake Addin
#tool nuget:?package=TCIS.EventBus&version=1.0.0-rc.18&prerelease
                    
Install as a Cake Tool

TCIS.EventBus

Transactional messaging for the TCIS ecosystem: an outbox that writes the message in the same transaction as your business data, a background sender, and a consumer side with retries and a poison-message path.

Transport bindings live in separate packages — see TCIS.EventBus.Kafka. For choosing between mediator.Publish and the EventBus, see TCIS.Mediator.


Table of contents

Section Contents
1 Registration
2 Publishing
3 Subscribing
4 Headers and context propagation
5 ⚠️ What happens to a message that fails
6 Consuming from a third-party producer
7 Storage choices
8 Delivery guarantees
9 Options reference
10 Pitfalls

1. Registration

builder.Services.AddTEventBus(options =>
{
    builder.Configuration.GetSection("TCIS:EventBus").Bind(options);

    options.UseSqlServer(connectionString);      // outbox lives next to your data
    options.UseKafka(kafka =>
        builder.Configuration.GetSection("TCIS:EventBus:Kafka").Bind(kafka));
});

Three things must be registered or startup throws: the bus itself, one storage provider, and one transport provider.

{
  "TCIS": {
    "EventBus": {
      "DefaultGroupName": "tcis.queue.vessel-service",
      "Version": "v1",
      "FailedRetryCount": 10,
      "FailedRetryInterval": 30,
      "SucceedMessageExpiredAfter": 86400,
      "ConsumerShutdownTimeoutSeconds": 5,
      "Kafka": {
        "Servers": "broker:9092",
        "MainConfig": { "group.id": "tcis.subscriber.vessel-group" }
      }
    }
  }
}

2. Publishing

public sealed class BerthVesselHandler(IEventPublisher bus, VesselDbContext db)
{
    public async Task Handle(BerthVessel cmd, CancellationToken ct)
    {
        // The bus joins the EF transaction: the outbox row and the business row commit together.
        using var tx = db.Database.BeginTransaction(bus, autoCommit: false);

        visit.Status = "BERTHED";
        await db.SaveChangesAsync(ct);

        await bus.PublishAsync("vessel.berthed", new VesselBerthed(visit.Id, visit.BerthCode));

        await tx.CommitAsync(ct);
    }
}

BeginTransaction(publisher, autoCommit) is an extension shipped by the storage package (TCIS.EventBus.SqlServer, TCIS.EventBus.PostgreSql); overloads exist for DatabaseFacade and for a raw IDbConnection.

The point of the outbox is that PublishAsync does not talk to the broker. It writes a row in the same transaction. If the transaction rolls back, the message was never published. If the process dies after commit but before send, a background processor picks the row up and sends it.

Publishing outside a transaction is allowed and sends immediately after persisting — use it only when there is no business data to keep in step.


3. Subscribing

public sealed class VesselSubscriber(IVesselService service) : IEventSubscribe
{
    [EventSubscribe("vessel.berthed")]
    public async Task OnBerthed(VesselBerthed evt, CancellationToken ct)
    {
        await service.RecordBerthingAsync(evt.VisitId, evt.BerthCode, ct);
    }
}

builder.Services.AddScoped<VesselSubscriber>();   // discovery reads DI registrations

Subscribers are discovered from the service collection, so the class must be registered — an unregistered subscriber is not an error, it simply never runs.

Each subscriber runs in its own DI scope, not the publisher's. A throwing subscriber does not affect the publisher; it affects only its own message.


4. Headers and context propagation

Ambient context travels in headers, so subscribers do not need it in the payload:

Header Carries
t-msg-id Message identity — the key for consumer-side idempotency
t-msg-name Topic
t-msg-group Internal group used to find the subscriber — not the Kafka group.id
t-tenant-id, t-site-code Multi-tenancy context, restored into IWorkContext
t-user-id, t-username, t-caller Audit context
t-corr-id, traceparent Correlation and distributed tracing

Keep the DTO clean — do not copy TenantId or SiteCode into the payload.


5. ⚠️ What happens to a message that fails

Three distinct outcomes. Confusing them is the usual reason an incident takes hours instead of minutes.

The subscriber throws

Retried in place up to 3 times, then handed to the background retry processor, which retries every FailedRetryInterval seconds up to FailedRetryCount times. After that the message stays Failed in storage and FailedThresholdCallback fires.

The message cannot be routed or deserialized

No subscriber matches the topic+group, or the payload does not parse. Retrying cannot help — the next attempt fails identically. The message is stored as an exception message, the offset is committed, and the stream continues. One Error log line records the reason:

Message parked and skipped: it could not be routed or deserialized, so the offset was committed
to let the stream continue. Id:…, Name:…, Group:…, Reason:SubscriberNotFoundException-->…

The payload parses but does not match the DTO

Nothing is reported. System.Text.Json ignores unknown JSON properties and leaves absent ones at their default, so the subscriber receives a half-empty object and the message is marked Succeeded. Property names are matched case-insensitively — but a genuinely different name (vessel_name versus VesselName) still will not bind.

This one has no library-level defence. Validate required fields inside the subscriber:

if (string.IsNullOrWhiteSpace(evt.VesselName))
    throw new TValidationException("VALIDATION_MISSING_VESSEL_NAME", "vessel_name is empty");

That converts a silent empty row into a parked message with a log line.


6. Consuming from a third-party producer

External producers do not emit TCIS headers. The transport fills in what is missing — see the Kafka README for how t-msg-id is derived and why it stays stable across redeliveries.

Two things to get right on the TCIS side:

  1. Do not set TopicNamePrefix. It would rewrite the partner's topic name into prefix.partner.topic, which does not exist.
  2. Match the payload explicitly. Declare [JsonPropertyName("...")] on the DTO rather than hoping the partner's naming matches yours. When the schema is not yet known, take a JsonElement first and log raw.GetRawText() to see exactly what arrives.

7. Storage choices

SQL Server / PostgreSQL InMemory
Outbox survives restart ❌ un-sent messages are lost
Retry state survives restart
Parked messages queryable ❌ RAM only
Cluster-wide retry locking ✅ via UseStorageLock

InMemory is for development and for stateless gateways where losing an un-sent message is acceptable. Anything with business meaning belongs in a durable store.

Two InMemory specifics worth knowing:

  • Messages are held for SucceedMessageExpiredAfter (24 h by default) before the collector removes them. That is 24 hours of messages resident in RAM — lower it for a high-volume consumer.
  • The dictionaries are static, so every host in the same process shares one store. This is fine for a single application, but it means two hosts side by side (or parallel integration tests) see each other's messages.

8. Delivery guarantees

At-least-once, with one condition. The offset is committed only after the subscriber finishes, so a crash mid-processing means the broker redelivers.

That condition breaks when EnableSubscriberParallelExecute = true: messages are then handed to an in-memory channel and the offset is committed straight away. A crash loses whatever is still in the channel. Enable it only when the throughput matters more than those messages do.

Because delivery is at-least-once, subscribers must be idempotent. Use t-msg-id as the deduplication key.


9. Options reference

Option Default Notes
DefaultGroupName tcis.queue.{entry-assembly} Suffixed with .{Version} to form the internal group
Version v1 Bump to run a new consumer generation alongside the old one
FailedRetryCount 50 Total attempts before a message is left Failed
FailedRetryInterval 60 Seconds between background retries
SucceedMessageExpiredAfter 86400 Seconds a succeeded message is retained
FailedMessageExpiredAfter 1296000 15 days
ConsumerThreadCount 1 Listening threads per group
ConsumerShutdownTimeoutSeconds 5 Keep below the host's own shutdown timeout
EnableSubscriberParallelExecute false See §8 before enabling
UseStorageLock false Required for correct retry behaviour across multiple instances
TopicNamePrefix null Never set this when consuming a partner's topic
JsonSerializerOptions case-insensitive Aligned with ASP.NET Core; see §5

10. Pitfalls

# Pitfall Consequence
1 Forgetting to register the subscriber class in DI It is never discovered and never runs — silently
2 Expecting PublishAsync to reach the broker immediately It writes to the outbox; the send happens afterwards
3 Publishing after the transaction commits Loses the atomicity the outbox exists to provide
4 Assuming a subscriber failure fails the publisher It does not — separate scope, separate lifetime
5 Non-idempotent subscribers At-least-once delivery will double-process on redelivery
6 Confusing t-msg-group with the Kafka group.id Overwriting the former makes every message unroutable
7 Setting TopicNamePrefix for a partner topic Subscribes to a topic that does not exist
8 Trusting that a parsed payload is a complete one Missing fields bind to defaults with no error — validate in the subscriber
9 InMemory storage in production Un-sent outbox rows and retry state are lost on restart
Product 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 was computed.  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 was computed.  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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (5)

Showing the top 5 NuGet packages that depend on TCIS.EventBus:

Package Downloads
TCIS.EventBus.InMemoryStorage

TCIS Core Framework is an application framework for building modular, multi-tenant applications on ASP.NET Core. In-memory storage provider for TCIS Event Bus.

TCIS.EventBus.Kafka

TCIS Core Framework is an application framework for building modular, multi-tenant applications on ASP.NET Core. Kafka provider for TCIS Event Bus.

TCIS.EventBus.SqlServer

TCIS Core Framework is an application framework for building modular, multi-tenant applications on ASP.NET Core. SQL Server storage provider for TCIS Event Bus.

TCIS.EventBus.PostgreSql

TCIS Core Framework is an application framework for building modular, multi-tenant applications on ASP.NET Core. PostgreSQL storage provider for TCIS Event Bus.

TCIS.EventBus.RabbitMQ

TCIS Core Framework is an application framework for building modular, multi-tenant applications on ASP.NET Core. RabbitMQ provider for TCIS Event Bus.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.0-rc.19 47 8/13/2026
1.0.0-rc.18 46 8/13/2026
1.0.0-rc.17 47 8/13/2026
1.0.0-rc.16 49 8/13/2026
1.0.0-rc.15 60 8/12/2026
1.0.0-rc.14 61 8/12/2026
1.0.0-rc.13 59 8/11/2026
1.0.0-rc.12 69 8/10/2026
1.0.0-rc.11 83 7/28/2026
1.0.0-rc.10 79 7/24/2026
1.0.0-rc.9 72 7/21/2026
1.0.0-rc.8 75 7/21/2026
1.0.0-rc.7 82 7/17/2026
1.0.0-rc.6 89 7/7/2026
1.0.0-rc.5 92 7/7/2026
1.0.0-rc.4 92 6/24/2026
1.0.0-rc.2 89 5/12/2026
1.0.0-rc.1 95 5/12/2026