Whisper 1.0.7

There is a newer version of this package available.
See the version list below for details.
dotnet add package Whisper --version 1.0.7
                    
NuGet\Install-Package Whisper -Version 1.0.7
                    
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="Whisper" Version="1.0.7" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Whisper" Version="1.0.7" />
                    
Directory.Packages.props
<PackageReference Include="Whisper" />
                    
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 Whisper --version 1.0.7
                    
#r "nuget: Whisper, 1.0.7"
                    
#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 Whisper@1.0.7
                    
#: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=Whisper&version=1.0.7
                    
Install as a Cake Addin
#tool nuget:?package=Whisper&version=1.0.7
                    
Install as a Cake Tool

Whisper

Whisper is a minimal-impact domain event tracking library for .NET.
It lets rich domain models raise domain events with no pollution β€” no Events property on aggregates, no plumbing in entities, and no domain-level awareness of dispatching or persistence.

Under the hood, Whisper uses AsyncLocal<T> to safely track domain events across both synchronous and asynchronous execution flows.
Events raised during a logical operation (e.g., a MediatR request, an NServiceBus message handler, or an ASP.NET Core request) remain attached to that flow until they are dispatched or persisted by outer layers.

Whisper integrates cleanly with DDD and Clean Architecture by keeping the domain pure and moving event handling to the application and infrastructure layers.
It also provides an optional outbox with MongoDB and SQL Server support, plus drop-in packages for MediatR and integration with NServiceBus unit of work.

πŸ“– Deep dive article: Minimal-impact domain events


Why Whisper?

Traditional domain event patterns often force you to:

  • Add an Events collection to every aggregate and bubble those events up.
  • Hand-roll async/thread context handling, or rely on brittle statics.
  • Mix domain logic with dispatching, messaging, or persistence concerns.

Whisper avoids all of that.

  • Clean domain β€” no Events list and no infrastructure references in your entities.
  • Async-safe tracking β€” uses AsyncLocal so events flow across awaits.
  • Infrastructure integration β€” MediatR, NServiceBus, MongoDB, and SQL Server support.
  • Zero ceremony β€” a single static call to raise an event from anywhere in your domain.

Packages

Package Purpose
Whisper Core tracking logic (Whispers, IDomainEvent, scopes)
Whisper.Abstractions Shared contracts (IWhisperBuilder, IDispatchDomainEvents)
Whisper.MediatR MediatR integration β€” automatically dispatches raised events after each request
Whisper.AspNetCore AspNetCore integration β€” automatically dispatches raised events after each request
Whisper.Outbox Outbox infrastructure + background worker and installer
Whisper.Outbox.MongoDb MongoDB outbox store + optional unit-of-work participation via IMongoSessionProvider
Whisper.Outbox.SqlServer SQL Server outbox store + optional unit-of-work participation via IConnectionLeaseProvider
Whisper.Outbox.AspNetCore Management dashboard + JSON API for failed outbox records (MapWhisperOutbox)
Whisper.Outbox.MongoDb.NServiceBus Adapter to reuse the NServiceBus Mongo storage session
Whisper.Outbox.SqlServer.NServiceBus Adapter to reuse the NServiceBus SQL storage session

Licensed under MIT.


How it works (high level)

Whisper keeps a per-execution β€œdomain event scope” in an AsyncLocal<T>:

  1. In your domain, you raise events:
    Whispers.About(new OrderApproved(orderId));
    
  2. Whisper attaches those events to the current async flow.
  3. In your application / infrastructure layer, Whisper (or your configured integration) retrieves and dispatches/persists the collected events at the right time β€” e.g., after a MediatR pipeline completes or via the outbox worker.

Your domain never exposes an events collection and never learns about dispatching, messaging, or persistence.


Core usage

1) Define a domain event

using Whisper;

public sealed record OrderApproved(Guid OrderId) : IDomainEvent;

2) Raise it from anywhere in the domain

using Whisper;

public class Order
{
    public Guid Id { get; }

    public void Approve()
    {
        // Domain logic...
        Whispers.About(new OrderApproved(Id));
    }
}

3) Optional: scopes for isolation

If you want isolation per request/unit of work, create a scope. Events raised inside are collected and can be read/cleared as needed:

using Whisper;

using var scope = Whispers.CreateScope();
// ... domain operations that raise events
var raised = Whispers.GetAndClearEvents(); // events for this scope (and children)

Most users don’t need to manage scopes explicitly β€” integrations (like MediatR) take care of flushing at the right time.


MediatR integration

When you call b.AddMediatR() inside the Whisper builder, Whisper registers:

  • An IDispatchDomainEvents implementation that publishes via IMediator.
  • A MediatR pipeline behavior that automatically retrieves and dispatches all raised domain events after each request.
using Whisper.Abstractions;
using Microsoft.Extensions.DependencyInjection;

var services = new ServiceCollection()
    .AddMediatR(c => c.RegisterServicesFromAssemblyContaining<Program>())
    .AddWhisper(b =>
    {
        b.AddMediatR(); // wires up dispatcher + automatic behavior
    });

AspNetCore integration

When you call app.UseDomainEventDispatcherMiddleware() on the IApplicationBuilder of your AspNetCore host, Whisper registers:

  • A conventional DomainEventDispatcherMiddleware implementation that publishes via the registered IDispatchDomainEvents implementations.
using Microsoft.AspNetCore.Builder;

app.UseDomainEventDispatcherMiddleware();

Outbox & transactions

Whisper provides an outbox for reliable, asynchronous dispatch:

  • Persists raised events to a durable store.
  • A background worker reads pending records and dispatches them via your configured IDispatchDomainEvents.
  • Supports MongoDB and SQL Server.
  • Optionally participates in your unit of work via very small provider abstractions.

Unit-of-work participation is opt-in, enabled explicitly via builder methods β€” ob.UseConnectionLeaseProvider<T>() (SQL Server) and ob.UseMongoSessionProvider<T>() (MongoDB). Ownership follows origin: Whisper disposes only what it creates, and it creates only when no provider is registered.

  • No provider registered: Whisper manages its own database access. For SQL Server it opens and disposes its own SqlConnection per operation, with no transaction; for MongoDB it uses plain writes with no IClientSessionHandle (and therefore no replica set requirement). Outbox records commit independently of any host transaction.
  • Provider registered: the host owns the connection/transaction/session entirely β€” Whisper only uses what the provider yields and never opens, begins, commits, rolls back, or disposes it.

The background worker always reads and dispatches after the unit of work completes.

MongoDB outbox

using Whisper.Abstractions;
using Microsoft.Extensions.DependencyInjection;

var services = new ServiceCollection()
    .AddMediatR(c => c.RegisterServicesFromAssemblyContaining<Program>())
    .AddWhisper(b =>
    {
        b.AddMediatR();

        b.AddOutbox(ob =>
        {
            ob.AddMongoDb(new()
            {
                ConnectionString = "mongodb://localhost:27017",
                DatabaseName     = "appdb",
                // CollectionName = "outboxrecords" // optional, default shown
            });
        });
    });

AddMongoDb also has an overload taking a Func<IServiceProvider, MongoDbOutboxConfiguration> for configuration resolved from DI.

IMongoSessionProvider (optional) lets the dispatch-side Add participate in your MongoDB session/transaction:

using MongoDB.Driver;

public interface IMongoSessionProvider
{
    public bool IsInTransaction => Session is not null;

    IClientSessionHandle? Session { get; }
}

There are no Commit / Abort methods on this interface.
When Session is null, Whisper writes without a session; otherwise it passes the session to its writes and the host owns it β€” Whisper never starts, commits, aborts, or disposes it.

Default β€” no session

AddMongoDb alone registers no provider. Outbox records are written as plain inserts β€” no IClientSessionHandle, no replica set requirement β€” and commit independently of any host transaction.

b.AddOutbox(ob => ob.AddMongoDb(new() { /* ... */ }));

Host-managed session

For the common β€œmy unit of work starts the session” case, Whisper ships a built-in provider pair. Register it with UseHostManagedMongoSession(), then hand your session over via IMongoSessionProviderInitializer after starting it:

b.AddOutbox(ob =>
{
    ob.AddMongoDb(new() { /* ... */ });
    ob.UseHostManagedMongoSession();
});
using Microsoft.Extensions.DependencyInjection;
using MongoDB.Driver;
using Whisper.Outbox.MongoDb;

// Inside your unit of work, after starting the session:
using var session = await mongoClient.StartSessionAsync(cancellationToken: ct);
session.StartTransaction();

serviceProvider.GetRequiredService<IMongoSessionProviderInitializer>().Initialize(session);

// ... domain operations β€” the outbox `Add` now joins `session`

await session.CommitTransactionAsync(ct); // the host owns commit/abort

A scope that never calls Initialize gets plain (session-less) writes.

NServiceBus storage session (requires Whisper.Outbox.MongoDb.NServiceBus)

b.AddOutbox(ob =>
{
    ob.AddMongoDb(new() { /* ... */ });
    ob.UseNServiceBusStorageSession(); // IMongoSessionProvider backed by the NServiceBus storage session
});

Internally routes through UseMongoSessionProvider, so the same ownership rule applies: NServiceBus owns the session; Whisper only uses it. May be called before or after AddMongoDb β€” registration is order-independent.

Custom unit of work

Return the session your unit of work already owns; the unit of work keeps ownership of commit/abort:

using MongoDB.Driver;
using Whisper.Outbox.MongoDb;

public sealed class UnitOfWorkMongoSessionProvider(MyUnitOfWork unitOfWork) : IMongoSessionProvider
{
    public IClientSessionHandle? Session => unitOfWork.Session;
}
b.AddOutbox(ob =>
{
    ob.AddMongoDb(new() { /* ... */ });
    ob.UseMongoSessionProvider<UnitOfWorkMongoSessionProvider>();
});

UseMongoSessionProvider also has an overload taking a Func<IServiceProvider, TProvider> factory:
ob.UseMongoSessionProvider(sp => new UnitOfWorkMongoSessionProvider(sp.GetRequiredService<MyUnitOfWork>()));
Registering IMongoSessionProvider directly in DI is honored too, but the builder method is the intended path.

SQL Server outbox

using Whisper.Abstractions;
using Microsoft.Extensions.DependencyInjection;

var services = new ServiceCollection()
    .AddMediatR(c => c.RegisterServicesFromAssemblyContaining<Program>())
    .AddWhisper(b =>
    {
        b.AddMediatR();

        b.AddOutbox(ob =>
        {
            ob.AddSqlServer(new()
            {
                ConnectionString = "Server=.;Database=AppDb;Trusted_Connection=True;Encrypt=False;",
                SchemaName       = "outbox",        // optional, default "dbo"
                TableName        = "OutboxRecords"  // optional, default "outboxrecords"
            });
        });
    });

IConnectionLeaseProvider (optional) lets the dispatch-side Add use your SQL connection + transaction:

using Microsoft.Data.SqlClient;

public interface IConnectionLeaseProvider
{
    ValueTask<ConnectionLease> Provide(CancellationToken cancellationToken);
}

public sealed record ConnectionLease(SqlConnection Connection, SqlTransaction? Transaction = null);

Ownership by origin: Whisper disposes only what it creates, and it creates only when no provider is registered.
ConnectionLease is pure data β€” everything a provider yields belongs to the host; Whisper only uses the connection and transaction and never opens, commits, rolls back, or disposes them.

Default β€” own connection per operation

AddSqlServer alone registers no provider. Without one, Whisper opens a fresh SqlConnection per operation, uses no transaction, and disposes the connection itself. Outbox records commit independently of any host transaction.

b.AddOutbox(ob => ob.AddSqlServer(new() { /* ... */ }));

TransactionScope β€” zero-code opt-in

On this no-provider path, the SqlConnection Whisper opens enlists in an ambient System.Transactions.TransactionScope by default (Enlist=true). Wrap the use case in a scope and outbox writes plus your own SQL join one transaction that the host completes:

using var tx = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled);
// ... host SQL work + domain operations that raise events
tx.Complete();

TransactionScopeAsyncFlowOption.Enabled is required with async code. Sequential opens against the same connection string reuse the transaction-affiliated pooled connection, so a single SQL Server does not escalate to MSDTC β€” but concurrent connections inside the scope can escalate to a distributed transaction.

NServiceBus storage session (requires Whisper.Outbox.SqlServer.NServiceBus)

b.AddOutbox(ob =>
{
    ob.AddSqlServer(new() { /* ... */ });
    ob.UseNServiceBusStorageSession(); // IConnectionLeaseProvider backed by the NServiceBus storage session
});

Internally routes through UseConnectionLeaseProvider, so the same ownership rule applies: NServiceBus owns the connection and transaction; Whisper only uses them. May be called before or after AddSqlServer β€” registration is order-independent.

Custom unit of work

Return the connection and transaction your unit of work already owns; the unit of work keeps ownership of commit/rollback and disposal:

using Whisper.Outbox.SqlServer;

public sealed class UnitOfWorkConnectionLeaseProvider(MyUnitOfWork unitOfWork) : IConnectionLeaseProvider
{
    public ValueTask<ConnectionLease> Provide(CancellationToken cancellationToken)
        => ValueTask.FromResult(new ConnectionLease(unitOfWork.Connection, unitOfWork.Transaction));
}
b.AddOutbox(ob =>
{
    ob.AddSqlServer(new() { /* ... */ });
    ob.UseConnectionLeaseProvider<UnitOfWorkConnectionLeaseProvider>();
});

Warning β€” every scope, no fallback: a registered IConnectionLeaseProvider is consulted for every store operation, including the background worker’s own polling scopes, where no unit of work is active. There is deliberately no fallback. Your provider must return a usable, open connection in every scope β€” when no unit of work is active, open one yourself (e.g., from configuration); otherwise the worker can never read or dispatch outbox records. This applies to the SQL provider only: the Mongo equivalent handles it naturally (Session is null β†’ plain writes).

UseConnectionLeaseProvider also has an overload taking a Func<IServiceProvider, TProvider> factory:
ob.UseConnectionLeaseProvider(sp => new UnitOfWorkConnectionLeaseProvider(sp.GetRequiredService<MyUnitOfWork>()));
Registering IConnectionLeaseProvider directly in DI is honored too, but the builder method is the intended path.

Worker configuration & retry semantics

The background worker polls the store on a fixed interval. Configure it via ConfigureWorker:

b.AddOutbox(ob =>
{
    ob.AddSqlServer(new() { /* ... */ });
    ob.ConfigureWorker(w =>
    {
        w.BatchSize         = 10;   // default
        w.PollingIntervalMs = 1000; // default
    });
});

The worker reads records that are due: not dispatched, not failed, and NextRetryAtUtc null or at/before now β€” ordered by NextRetryAtUtc ascending (nulls first), then Id ascending. With no retry delay configured this is identical to plain Id order. Per record in a batch:

  • Success: the record is marked DispatchedAtUtc.
  • Deserialization failure is permanent: the record is marked FailedAtUtc immediately β€” no retry.
  • Dispatch failure: an unrecoverable exception (per the recoverability policy) fails the record immediately; otherwise Retries is incremented and the next attempt is scheduled via NextRetryAtUtc; once Retries + 1 >= MaxRetries, the record is marked FailedAtUtc.
  • Every failed attempt persists the error on the record: LastError (Exception.ToString(), truncated to 32,768 characters) and LastErrorAtUtc β€” so you can always see why a record is retrying or failed.

The worker loop catches and logs all errors and keeps polling.

Failed records stay in the store β€” there is no automatic cleanup. Monitor them yourself or use the management dashboard to inspect, retry, or delete them.

Recoverability

ConfigureRecoverability controls how failed dispatch attempts are handled β€” maximum attempts, the delay before the next attempt, and which exceptions are unrecoverable:

b.AddOutbox(ob =>
{
    ob.AddSqlServer(new() { /* ... */ });
    ob.ConfigureRecoverability(r =>
    {
        r.MaxRetries = 3;                                                     // default; total dispatch attempts
        r.RetryDelay = attempt => TimeSpan.FromSeconds(Math.Pow(2, attempt)); // exponential backoff
        r.UnrecoverableExceptionTypes.Add(typeof(NotSupportedException));
        r.UnrecoverableExceptionPredicates.Add(ex => ex.Message.Contains("permanent"));
    });
});
  • MaxRetries (default 3) β€” maximum total dispatch attempts; a record fails permanently when Retries + 1 >= MaxRetries. Moved here from OutboxWorkerOptions (breaking).
  • RetryDelay β€” maps the 1-based failed-attempt ordinal to the delay before the next attempt. null (the default) or a non-positive delay means the record is eligible again at the next poll β€” the previous behavior.
  • UnrecoverableExceptionTypes β€” exception types (including derived types) that permanently fail a record on the first occurrence.
  • UnrecoverableExceptionPredicates β€” predicates that mark an exception unrecoverable.

Polling granularity: the effective retry moment is the first poll at or after NextRetryAtUtc, so delays shorter than PollingIntervalMs behave like immediate (next-poll) retries.

A throwing predicate is logged and treated as non-matching (recoverable); a throwing RetryDelay is logged and falls back to a next-poll retry.

Serializer configuration

Domain events are serialized with System.Text.Json. Use ConfigureSerializer to register JsonConverters for value types (e.g., readonly record structs) that System.Text.Json cannot deserialize using the default parameterless constructor:

b.AddOutbox(ob =>
{
    ob.AddSqlServer(new() { /* ... */ });
    ob.ConfigureSerializer(s => s.Converters.Add(new MyValueTypeConverter()));
});

Startup & installation

AddOutbox registers an installer hosted service that prepares the store at startup:

  • SQL Server: creates the schema (if missing) and the table with a clustered primary key on Id, UTC check constraints, the filtered ready index IX_Outbox_Ready_ByDue, and the failed-records index IX_Outbox_Failed_ByFailedAt.
  • MongoDB: creates the partial index ix_outbox_ready_by_due and the index ix_outbox_failed_by_failedat on the outbox collection.

Existing tables/collections are upgraded in place: the installer idempotently adds the new columns (LastError, LastErrorAtUtc, NextRetryAtUtc) and indexes and drops the legacy undispatched index. On SQL Server this runs as two sequential batches (columns first, then constraints/indexes) so upgrades from older schemas compile cleanly.

Dispatches issued before installation completes are gated, not failed β€” they wait until the installer signals completion. The background worker waits the same way before its first poll.

UUID generation

AddOutbox registers a default IUuidProvider that generates database-friendly UUIDs (via UUIDNext); AddSqlServer deterministically replaces it with a SQL Server-specific provider that generates sequential GUIDs, friendly to the clustered primary key.

To override it, implement IUuidProvider (Guid Provide()) and register your implementation after the AddWhisper(...) call so it wins over the backend’s replacement:

services.AddWhisper(b => b.AddOutbox(ob => ob.AddSqlServer(...)));
services.Replace(ServiceDescriptor.Transient<IUuidProvider, MyUuidProvider>());

Outbox records are read and dispatched due-first β€” ordered by NextRetryAtUtc (nulls first), then Id; identical to plain Id order when no retry delay is configured. On SQL Server Id is the clustered primary key β€” a non-sequential custom provider changes dispatch ordering behavior and fragments the clustered index.

Management dashboard

The Whisper.Outbox.AspNetCore package adds a host-mounted management dashboard for failed outbox records:

using Microsoft.AspNetCore.Builder;

app.MapWhisperOutbox();                        // mounts at /whisper/outbox
// or: app.MapWhisperOutbox("/admin/outbox");  // custom pattern
Endpoint Purpose
GET / The dashboard page
GET /api/failed?page&pageSize Page failed records, FailedAtUtc descending (pageSize clamped to 1..200)
GET /api/records/{id} Full record incl. Payload and LastError (404 on miss)
POST /api/records/{id}/retry Re-queue a failed record (204 / 404)
POST /api/failed/retry-all Re-queue all failed records β†’ { "retried": n }
DELETE /api/records/{id} Delete a failed record (204 / 404)

Retrying resets FailedAtUtc, Retries, and NextRetryAtUtc but keeps LastError as an audit trail. Retry and delete only affect records that are actually failed, so the worker and the dashboard can never fight over a record.

Secure by default. Every endpoint is mapped with RequireAuthorization(); opt out explicitly via app.MapWhisperOutbox(configure: o => o.AllowAnonymous = true). A host without authentication/authorization configured gets an InvalidOperationException on the first request β€” an intended fail-safe, because the dashboard exposes event payloads. MapWhisperOutbox returns an IEndpointConventionBuilder for policies of your own, and throws at map time when no outbox storage backend (and therefore no IOutboxManagementStore) is registered.

The embedded page relies on inline <script>/<style>. If your host enforces a strict Content-Security-Policy, allow inline script and style for the dashboard route.


Clean Architecture fit

  • Domain
    References only Whisper. Raises events via Whispers.About(...).
    No Events collection, no leakage of domain events, no knowledge of dispatching or outbox.

  • Application
    Orchestrates operations; MediatR behavior (from Whisper.MediatR) automatically flushes events.

  • Infrastructure
    Configures outbox storage (Mongo/SQL), transaction providers, and messaging integrations (e.g., NServiceBus).

  • Presentation/API
    AspNetCore middleware to scope domain events per request.


AddWhisper API (DI)

Whisper uses a tiny builder to register components.

services.AddWhisper(b =>
{
    b.AddMediatR();
    b.AddOutbox(ob =>
    {
        ob.AddMongoDb(...);
        // or:
        // ob.AddSqlServer(...);
        // optional:
        // ob.UseNServiceBusStorageSession();
    });
});

Signature

public static IServiceCollection AddWhisper(
    this IServiceCollection services,
    Action<Whisper.Abstractions.IWhisperBuilder> configure);

IWhisperBuilder exposes the underlying IServiceCollection so you can extend/customize the setup.


Core API reference

Whispers (Whisper)

Method Description
IDisposable CreateScope() Creates an ambient scope (nestable).
void About(IDomainEvent domainEvent) Raises a domain event from anywhere in the domain.
IDomainEvent[] Peek() Inspect currently raised events without clearing them.
IDomainEvent[] GetAndClearEvents() Retrieve and clear the collected events for the current (deepest) scope.

IDispatchDomainEvents (Whisper.Abstractions)

public interface IDispatchDomainEvents
{
    Task Dispatch(IDomainEvent domainEvent, CancellationToken cancellationToken);
    Task Dispatch(IDomainEvent[] domainEvents, CancellationToken cancellationToken);
}

Whisper.MediatR provides an implementation that publishes via IMediator, and includes an automatic behavior that flushes raised events after each pipeline execution.


FAQ (quick)

Do I need to add an Events list to my aggregates?
No. Raise events with Whispers.About(...) and let Whisper collect them.

Is it safe across async/await?
Yes. Whisper uses AsyncLocal<T> to keep events bound to the current async execution flow.

How do duplicates get handled?
Whisper does not attempt to deduplicate; if you need deduplication, implement it at your dispatcher/consumer side. Outbox publishing is at-least-once: a record that was dispatched successfully but not yet marked (e.g., a crash between the dispatch and SetDispatchedAt) is re-dispatched on a later poll β€” consumers must be idempotent.

Who commits/rolls back the DB transaction for the outbox?
You do β€” if there is one. The rule: Whisper disposes only what it creates, and it creates only when no provider is registered. Without a provider there is no host transaction: Whisper opens and disposes its own SqlConnection per operation (SQL Server) or performs plain writes (MongoDB), and outbox records commit independently. With a provider registered (UseConnectionLeaseProvider / UseMongoSessionProvider), the host owns the connection/transaction/session entirely β€” Whisper only uses them and never begins, commits, rolls back, or disposes anything. The worker dispatches after your unit of work completes (at-least-once).

Where do I see why an event failed?
On the record itself. Every failed dispatch attempt persists the exception on the outbox record β€” LastError (Exception.ToString(), truncated) and LastErrorAtUtc β€” while the record is retrying and after it fails permanently. Mount the management dashboard from Whisper.Outbox.AspNetCore (app.MapWhisperOutbox()) to browse failed records, inspect the error and payload, and retry or delete them.

Does MediatR require a custom behavior from me?
No. When you use b.AddMediatR(), Whisper adds its own behavior that dispatches raised events automatically.


Summary

  • Minimal domain impact: raise events anywhere, no aggregate Events list.
  • Async-safe: powered by AsyncLocal<T>.
  • MediatR auto-dispatch: built-in behavior flushes events after each request.
  • Outbox ready: MongoDB & SQL Server with transaction participation.
  • NServiceBus adapters: reuse existing storage sessions.
  • Clean Architecture aligned: domain stays pure; infrastructure handles dispatch/persistence.

Whisper β€” domain events without domain pollution.

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.
  • net8.0

    • No dependencies.

NuGet packages (2)

Showing the top 2 NuGet packages that depend on Whisper:

Package Downloads
Whisper.Abstractions

Domain event dispatching abstractions for the Whisper framework.

Whisper.MediatR

MediatR integration for domain events. Captures domain events raised during MediatR request handling and dispatches them as MediatR notifications.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.8 239 7/14/2026
1.0.7 65 7/13/2026
1.0.6 681 11/3/2025
1.0.5 169 10/31/2025
1.0.4 176 10/31/2025
1.0.3 320 10/28/2025
1.0.2 207 10/28/2025
1.0.1 198 10/28/2025
1.0.0 379 10/27/2025