Nexo

Documentation

Every option

A complete reference for each option, with its default and the reason for it.

NexoOptions

The configuration builder AddNexo hands you.

MemberDefaultDescription
HandlerLifetimeScopedThe lifetime of discovered handlers. Scoped matches the unit of work they write through.
MediatorLifetimeScopedIMediator's lifetime. Singleton is only accepted with singleton handlers: a singleton mediator would resolve scoped handlers from the root scope, and registration fails rather than letting that happen.
Services—The IServiceCollection, for registrations Nexo does not cover.
C#
services.AddNexo(nexo =>
{
    // Handler discovery
    nexo.AddHandlersFromAssemblyOf<Program>();
    nexo.AddHandlersFromAssemblyOf<Program>(filter: t => !t.IsNested);
    nexo.AddHandlersFromAssemblies(assemblyA, assemblyB);
    nexo.AddHandlersFromAssemblies(filter: t => !t.IsNested, assemblyA, assemblyB);
    nexo.AddHandler<OneSpecificHandler>();
    nexo.AddGeneratedHandlers();   // generated at compile time for this assembly

    // Pipelines (registering twice does not duplicate)
    nexo.AddLoggingPipeline(logging => { /* ... */ });
    nexo.AddTimeoutPipeline();     // for [MessageTimeout]
    nexo.AddEnrichmentPipeline();
    nexo.AddUnitOfWorkPipeline(uow => { /* ... */ });
    nexo.AddPipeline<MyPipeline>();
    nexo.AddPipeline<MyPipeline>(ServiceLifetime.Singleton);
    nexo.AddPipeline(sp => new MyPipeline(sp.GetRequiredService<IFoo>()));
    nexo.AddPipeline(new PipelineWithNoDependencies());

    // Mediator
    nexo.ConfigureMediator(m => m.EventDispatchMode = EventDispatchMode.Parallel);

    // The ambient user
    nexo.UseCurrentUser<HttpCurrentUserAccessor>();
    nexo.UseCurrentUser(sp => "system");

    // The entities' public identifier (default: UUID v7)
    nexo.UseExternalIdGenerator(ExternalIdGenerators.SqlServer);

    // Other databases, outbox and inbox (Nexo.EntityFrameworkCore)
    nexo.UseDbContext<BillingDbContext>();
    nexo.UseDbContext<ActivityDbContext>(session => session.UseTransactions = false);
    nexo.UseOutbox(outbox => outbox.PollingInterval = TimeSpan.FromSeconds(1));
    nexo.UseInbox();
});

MediatorOptions

OptionDefaultDescription
EventDispatchModeSequentialHow an event's handlers run. Sequential is the default because they share the scope, and therefore the DbContext, which is not thread-safe. Parallel aggregates failures through Task.WhenAll.

UnitOfWorkPipelineOptions

OptionDefaultDescription
SaveChangesOnCommandtrueSaves after the command handler returns. It is what lets the handler simply stop.
SaveChangesOnEventtrueSaves after an event published from outside. Events published from inside a command join its unit of work, regardless.
UseTransactiontrueWraps handler, save and events in one transaction, through the provider's execution strategy. Ignored on a provider without transactions.
DispatchDomainEventstrueCollects the events from the changed entities — before the save, so a removed entity's are not lost — and dispatches them after it, in the same transaction. Integration events go to the outbox, if there is one.
MaxDomainEventRounds4How many cascade rounds are allowed. Exceeding it aborts the transaction instead of looping.
WarnOnPendingQueryChangestrueLogs a warning when a query handler leaves pending changes — nearly always a write that leaked.

LoggingPipelineOptions

OptionDefaultDescription
LevelDebugThe level of the start and success entries.
FailureLevelErrorThe level when the handler throws something unexpected.
ExpectedFailureLevelInformationThe level for failures that are answers rather than incidents: validation, not found, a business rule, a duplicate key, a concurrency conflict, a requested cancellation.
IsExpectedFailurethe list aboveDecides which exceptions count as expected.
LogPayloadfalseOff on purpose. Commands carry passwords, tokens and personal data; a default that leaks that is a liability, not a feature.
LogResultfalseSerialises the result. Rarely worth the volume.
SerializerSystem.Text.JsonHow the payload becomes text. Replace it with a serialiser that redacts fields.

Every dispatch opens a log scope with NexoMessageKind, NexoMessageName and NexoMessageId, so any line written inside the handler comes out correlated already.

ValidationPipelineOptions

OptionDefaultDescription
ValidateCommandstrueCommands change state.
ValidateQueriestrueIt keeps filters and paging sane.
ValidateEventsfalseAn event describes something that already happened; rejecting it after the fact rarely helps.
FailIfValidatorNotFoundfalseTurn it on when the team decides every command must have a validator and you want the container to enforce it.

DbSessionOptions

One set per context. services.Configure<DbSessionOptions>(…) sets the default for every database; the callback on AddNexo<TContext> or UseDbContext<TContext> tunes one of them, starting from that default.

OptionDefaultDescription
SaveBehaviorUnitOfWorkWhen changes reach the database. Immediate saves on every operation: convenient in a script, but it gives up atomicity.
TrackReadstrueReads by key come back tracked. An injected IQueryable never tracks, regardless.
SoftDeletetrueIDelete marks ISoftDeletable entities instead of removing them.
ApplyAudittrueTurns on the AuditInterceptor's stamps.
AssignMissingExternalIdstrueAssigns an identifier on insert when the field is empty, using the generator from UseExternalIdGenerator.
UseTransactionsnullnull detects from the provider: relational yes, in-memory and MongoDB no. Set true for a MongoDB replica set. Without transactions, on a non-relational context, Nexo also turns off the provider's automatic transaction.
C#
// The default for every database.
services.Configure<DbSessionOptions>(session => session.TrackReads = true);

services.AddNexo<CatalogDbContext>(nexo => nexo
    // Inherits TrackReads = true, overrides the rest.
    .UseDbContext<ActivityDbContext>(session =>
    {
        session.UseTransactions = false;
        session.SoftDelete = false;
    }));

// Checking what actually applied, per database:
var sessions = scope.ServiceProvider.GetRequiredService<DbSessionProvider>();
sessions.ForEntity<ActivityEntry>().Options.SoftDelete;   // false
sessions.ForEntity<Product>().Options.SoftDelete;         // true

OutboxOptions

Passed to nexo.UseOutbox(outbox => …). See Outbox.

OptionDefaultDescription
RunDispatchertrueRegisters the background service that delivers the events. Turn it off to call OutboxProcessor.ProcessAsync from a scheduler of your own.
PollingInterval2 sThe gap between passes when nothing is pending.
BatchSize50How many events one pass tries to deliver.
MaxAttempts10After that the event stays in the table, with the last error, for a person to look at.
RetryDelay / MaxRetryDelay5 s / 10 minThe exponential back-off between attempts.
ClaimDuration1 minHow long one instance reserves an event it is delivering.
SerializerOptionsJsonSerializerDefaults.GeneralHow the event is written and read.

A complete setup, everything explicit

Program.cs
// One AddDbContext per database. Nexo never registers the context for you.
builder.Services.AddDbContext<CatalogDbContext>((sp, options) => options
    .UseSqlServer(
        builder.Configuration.GetConnectionString("Catalog"),
        sql => sql.EnableRetryOnFailure())
    .UseNexo(sp));

builder.Services.AddDbContext<PurchasingDbContext>((sp, options) => options
    .UseMySQL(
        builder.Configuration.GetConnectionString("Purchasing")!,
        mySql => mySql.EnableRetryOnFailure())
    .UseNexo(sp));

builder.Services.AddDbContext<ActivityDbContext>((sp, options) => options
    .UseMongoDB(builder.Configuration.GetConnectionString("Activity")!, "activity")
    .UseNexo(sp));

builder.Services.AddNexo<CatalogDbContext>(
    nexo =>
    {
        nexo.HandlerLifetime = ServiceLifetime.Scoped;
        nexo.MediatorLifetime = ServiceLifetime.Scoped;

        // The other databases, each with its own session options.
        nexo.UseDbContext<PurchasingDbContext>();
        nexo.UseDbContext<ActivityDbContext>(session =>
        {
            session.UseTransactions = false;
            session.SoftDelete = false;
        });

        nexo.AddHandlersFromAssemblyOf<Program>();
        nexo.AddValidatorsFromAssemblyOf<Program>();
        nexo.UseCurrentUser<HttpCurrentUserAccessor>();

        nexo.ConfigureMediator(mediator =>
            mediator.EventDispatchMode = EventDispatchMode.Sequential);

        nexo.AddPipeline<MetricsPipeline>(ServiceLifetime.Singleton);
    },
    // The session options for CatalogDbContext.
    session =>
    {
        session.SaveBehavior = SaveBehavior.UnitOfWork;
        session.TrackReads = true;
        session.SoftDelete = true;
        session.ApplyAudit = true;
    });

Configuring from appsettings

Every option goes through IOptions<T>, so it can come from configuration like anything else.

appsettings.json
{
  "Nexo": {
    "UnitOfWork": {
      "UseTransaction": true,
      "MaxDomainEventRounds": 4
    },
    "Logging": {
      "Level": "Information",
      "LogPayload": false
    }
  }
}
C#
builder.Services.Configure<UnitOfWorkPipelineOptions>(
    builder.Configuration.GetSection("Nexo:UnitOfWork"));

builder.Services.Configure<LoggingPipelineOptions>(
    builder.Configuration.GetSection("Nexo:Logging"));