TCIS.Mediator 1.0.0-rc.37

This is a prerelease version of TCIS.Mediator.
dotnet add package TCIS.Mediator --version 1.0.0-rc.37
                    
NuGet\Install-Package TCIS.Mediator -Version 1.0.0-rc.37
                    
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.Mediator" Version="1.0.0-rc.37" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="TCIS.Mediator" Version="1.0.0-rc.37" />
                    
Directory.Packages.props
<PackageReference Include="TCIS.Mediator" />
                    
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.Mediator --version 1.0.0-rc.37
                    
#r "nuget: TCIS.Mediator, 1.0.0-rc.37"
                    
#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.Mediator@1.0.0-rc.37
                    
#: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.Mediator&version=1.0.0-rc.37&prerelease
                    
Install as a Cake Addin
#tool nuget:?package=TCIS.Mediator&version=1.0.0-rc.37&prerelease
                    
Install as a Cake Tool

TCIS.Mediator

A small CQRS mediator for the TCIS ecosystem: requests, notifications, streams, and a deliberately minimal pipeline.

Per-site handler routing lives in TCIS.Mediator.Keyed.


Table of contents

Section Contents
1 Registration
2 Requests and handlers
3 The pipeline
4 Notifications
5 ⚠️ Publish vs EventBus — read before using either
6 Streams
7 Pitfalls

1. Registration

dotnet add package TCIS.Mediator
builder.Services.AddTMediator(options =>
{
    options.Assemblies = [typeof(Program).Assembly];   // scans handlers + validators
    options.AddTEcosystem();                            // tracing, then validation
});

Assembly scanning registers IRequestHandler<,>, INotificationHandler<>, IStreamRequestHandler<,> and FluentValidation IValidator<>.

IMediator is registered with TryAdd, so AddTMediator and AddTKeyedMediator may be called in either order without one silently overriding the other.


2. Requests and handlers

public sealed record CreateOrder(string OrderNo, string CustomerCode) : IRequest<Guid>;

public sealed class CreateOrderHandler(IUnitOfWork uow) : IRequestHandler<CreateOrder, Guid>
{
    public async Task<Guid> Handle(CreateOrder request, CancellationToken ct)
    {
        var order = new Order { OrderNo = request.OrderNo };
        uow.Repository<Order>().Add(order);
        await uow.SaveChangesAsync(ct);
        return order.Id;
    }
}

// Call site
Guid id = await mediator.Send(new CreateOrder("ORD-001", "CUST-01"), ct);

Use IRequest (no type argument) for commands with no return value — it is IRequest<Unit>.


3. The pipeline

Registration order IS execution order, outer to inner. The behavior registered first ends up outermost.

AddTEcosystem() registers exactly two:

TracingBehavior      ← outermost: the span covers the whole request
ValidationBehavior
handler              ← innermost

Add your own with options.AddBehavior(typeof(MyBehavior<,>)) — placed according to where you register it.

Why the standard pipeline is this small

Four behaviors were removed after an audit found that nothing in the repository — including all three templates and the sample — had ever implemented them:

  • ExceptionHandlerBehavior could turn an infrastructure failure into a fake success, violating the business/infrastructure boundary. TExceptionMiddleware already maps exceptions by taxonomy.
  • LoggingBehavior logged the whole request object at Information level — a login or change-password command would have written credentials to the log. Duration is already on the tracing span; 5xx logging is already in the middleware.
  • PreProcessorBehavior / PostProcessorBehavior ran on every request to iterate an empty collection. Work that belongs before or after a handler reads better inside the handler.

Validation

ValidationBehavior runs every registered IValidator<TRequest>. On failure it throws TValidationException with code VALIDATION_ERROR (HTTP 400) and per-field detail:

{
  "isSuccess": false,
  "code": "VALIDATION_ERROR",
  "message": "Validation failed: Name is required.",
  "data": [ { "field": "name", "message": "Name is required." } ]
}

It stays in the pipeline — rather than relying on the MVC validation filter — because it is the only place covering every ingress: MVC controllers, Minimal APIs, gRPC services, EventBus consumers and Hangfire jobs all reach the same handler (ARC-050). The MVC filter only runs for controller actions.


4. Notifications

public sealed record ContainerWeighed(Guid VisitId, string ContainerNo, int WeightKg) : INotification;

public sealed class UpdateVisitTotalHandler : INotificationHandler<ContainerWeighed> { … }
public sealed class WriteWeighAuditHandler  : INotificationHandler<ContainerWeighed> { … }

await mediator.Publish(new ContainerWeighed(visitId, "TCIU1234567", 28400), ct);

Every registered handler runs, sequentially, in DI registration order.

Publish is not fire-and-forget. The correct mental model is:

await mediator.Publish(evt, ct);
// ≡
foreach (var h in handlers) await h.Handle(evt, ct);

No queue, no separate scope, no retry. Handlers run on the caller's thread, in the caller's DI scope, sharing the caller's DbContext and transaction. A handler that throws fails the caller, and later handlers do not run.

Sequential is not a limitation to work around: handlers share one DbContext, which is not thread-safe. Dispatching them concurrently produces "A second operation was started on this context".


5. ⚠️ Publish vs EventBus — read before using either

One question decides it:

If this handler fails, SHOULD the original command fail too?

Yes → mediator.Publish · No → EventBus + Outbox

All four conditions must hold to use Publish. Break one and you need the EventBus:

# Condition Otherwise
1 The handler writes to the same database as the caller Another module, another DbContext — one transaction spans one context (ARC-021)
2 The handler does no I/O outside the database HTTP, email, broker calls put network latency inside a transaction (DB-036)
3 The handler failing should fail the command Otherwise you get a partial failure: data committed, client told it failed
4 Consistency is needed now, not eventually Eventual is fine → the EventBus is cheaper and safer

Where Publish genuinely earns its place

Several same-module reactions that must be atomic. The command does not need to know how many pieces of logic react.

An in-transaction extension point for plugins — the strongest reason in this stack. Platform publishes, a site plugin adds its own reaction, and both live or die together:

await mediator.Publish(new GateInCompleted(transactionId), ct);   // Platform

[/* plugin assembly */]
public sealed class CatLaiSurchargeHandler : INotificationHandler<GateInCompleted> { … }

With TCIS.Mediator.Keyed, Platform and plugin handlers both react. The EventBus cannot do this — it is eventual by design, so the plugin's reaction would land outside the transaction and change the business meaning.

Where it does not

Do not reach for Publish merely to "decouple". If there is exactly one handler and it must run, call the service directly: you get a compile-time contract, a readable call flow, and a debugger that steps straight through.

Publish earns its keep through plurality and openness — an unknown, extensible set of reactors. Without that, it is a method call hidden behind indirection.

Decision table

Task Use
Weigh a container → update the visit total + write an audit row Publish
Gate-in completes → a port plugin adds its own surcharge Publish
Vessel arrives → another module reacts EventBus
Invoice issued → send email, call the customs API EventBus
Voyage closed → generate a report EventBus

6. Streams

public sealed record ExportOrders(DateOnly From) : IStreamRequest<OrderRow>;

await foreach (var row in mediator.CreateStream(new ExportOrders(from), ct))
{
    await writer.WriteAsync(row);
}

Use for large exports where materialising the whole result would blow memory.


7. Pitfalls

# Pitfall Consequence
1 Expecting Publish to be asynchronous / fire-and-forget It is a synchronous fan-out; a failing handler fails the caller
2 Calling Publish after committing the transaction Partial failure: data saved, client told it failed. Publish before the commit, or use the EventBus
3 Handlers that call HTTP/email inside Publish Network latency held inside a database transaction
4 Adding the same behavior twice Guarded — AddBehavior is idempotent
5 Assuming a behavior registered last runs first The opposite: first registered is outermost
6 Writing a validator for the bound DTO instead of the command Only the HTTP path is protected; gRPC, consumers and jobs are not
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 (1)

Showing the top 1 NuGet packages that depend on TCIS.Mediator:

Package Downloads
TCIS.Mediator.Keyed

TCIS Core Framework is an application framework for building modular, multi-tenant applications on ASP.NET Core. Multi-tenant support for TCIS.Mediator using Keyed Services.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.0-rc.37 0 8/27/2026
1.0.0-rc.36 0 8/27/2026
1.0.0-rc.35 31 8/26/2026
1.0.0-rc.34 35 8/26/2026
1.0.0-rc.33 61 8/21/2026
1.0.0-rc.32 55 8/21/2026
1.0.0-rc.31 50 8/21/2026
1.0.0-rc.30 57 8/21/2026
1.0.0-rc.29 61 8/21/2026
1.0.0-rc.28 65 8/21/2026
1.0.0-rc.27 61 8/21/2026
1.0.0-rc.26 59 8/20/2026
1.0.0-rc.25 58 8/20/2026
1.0.0-rc.24 61 8/20/2026
1.0.0-rc.23 69 8/20/2026
1.0.0-rc.22 64 8/19/2026
1.0.0-rc.21 63 8/19/2026
1.0.0-rc.20 69 8/18/2026
1.0.0-rc.19 68 8/13/2026
1.0.0-rc.18 56 8/13/2026
Loading failed