Optima.Net.Application 3.0.1

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

Optima.Net.Application

TL;DR

Optima.Net.Application is the application-layer foundation of the Optima.Net ecosystem.

It defines contracts and orchestration boundaries, not infrastructure implementations.

If you are looking for:

  • where application use cases live
  • how transactional boundaries are coordinated
  • how commands and queries are represented
  • how aggregate persistence is abstracted
  • how Integration Events are stored and published
  • how idempotent processing is coordinated
  • how long-running sagas are represented and persisted

this is that package.

No database implementation. No message broker implementation. No EF Core. No Kafka. No AWS SDK. No magic.

Just application-layer contracts and small orchestration helpers.


What This Package Is

Optima.Net.Application provides reusable application-layer abstractions for Clean Architecture, Hexagonal Architecture, and Domain-Driven Design.

The application layer coordinates use cases and communicates with external capabilities through ports.

This package:

  • defines command and query handler contracts
  • defines aggregate persistence ports
  • defines transactional boundaries
  • defines idempotency coordination
  • defines Integration Event outbox and publishing ports
  • defines saga contracts and saga persistence
  • provides small execution extensions for commands, queries, and Units of Work

Infrastructure supplies implementations of persistence, messaging, transactions, and other external capabilities.

The package intentionally keeps those implementation details out of the application contract.


Dependencies

Optima.Net.Application depends on Optima.Net for foundational types such as:

  • Optional<T>
  • Result<TResult, TError>
  • Unit

Optima.Net.Application does not depend on Optima.Net.DomainModel.

Domain-specific abstractions, including abstractions based on Domain Facts, belong in packages or applications that explicitly depend on the domain model.


What This Package Is Not

This package does not:

  • implement persistence
  • implement message brokers
  • contain domain business rules
  • provide a validation framework
  • provide logging or metrics
  • provide retry policies
  • expose database-specific APIs
  • expose message-broker-specific APIs
  • provide Domain Fact infrastructure
  • hide transaction management behind ambient context

Infrastructure concerns remain infrastructure concerns.

Domain concerns remain domain concerns.


Design Philosophy

Contracts Over Implementations

Most of the package consists of application-layer interfaces.

The provided extensions perform small amounts of explicit orchestration over those contracts.

Infrastructure implementations remain outside the package.

Explicit Over Clever

Important application behavior should be visible in code.

You should be able to determine:

  • when a Unit of Work begins
  • when it commits
  • when it rolls back
  • when aggregate persistence is requested
  • when an Integration Event is added to an outbox
  • when an Integration Event is published
  • when idempotency is checked or recorded
  • when saga state is loaded or persisted

The package avoids ambient transaction contexts and hidden application behavior.

Expected Failures Are Values

Expected application failures are represented using:

Result<TResult, TError>

Unexpected runtime and infrastructure failures remain exceptions.

The package does not define a universal application error hierarchy.

Each application or use case remains free to define an appropriate TError.


Core Concepts

Commands

A command represents caller intent that may change application or domain state.

Examples:

  • CreateOrder
  • CancelOrder
  • ApprovePayment

A command handler coordinates the application use case.

Business rules and invariants belong in the domain model rather than the command handler.

Commands that do not return a meaningful value use Unit as their result type.

Queries

A query retrieves data without mutating application or domain state.

A query handler may:

  • retrieve data through application-layer ports
  • map data
  • project data into application results
  • return expected failures through Result

A query handler should not:

  • mutate application or domain state
  • invoke domain behavior that changes aggregate state
  • manage transaction lifecycles
  • depend directly on infrastructure-specific persistence APIs

The query abstraction does not prohibit infrastructure from using transactional or consistent-read mechanisms internally where appropriate.

Unit of Work

A Unit of Work defines a transactional boundary for an application operation.

Participating application-layer ports may stage persistence operations within that boundary.

A successful commit means the Unit of Work has taken durable responsibility for the participating operations.

Rollback abandons uncommitted work.

Rollback is not compensation and does not undo work that has already been committed.

Integration Events

An Integration Event is a message intentionally intended to cross an application or system boundary.

Integration Events are identified explicitly using IIntegrationEvent.

The package separates two responsibilities:

  • IBaseIntegrationEventOutbox provides durable responsibility for later publication.
  • IBaseIntegrationEventPublisher provides delivery to external messaging infrastructure.

Adding an Integration Event to an outbox does not publish it.

Publishing an Integration Event does not imply durable storage or transaction participation.

Idempotency

Idempotency prevents the same logical operation from being successfully applied more than once.

The idempotency store participates in the current Unit of Work.

An idempotency record becomes durable only when that Unit of Work commits.

Checking whether a key exists is useful for detecting previously completed operations, but it is not the concurrency guarantee.

Implementations must enforce uniqueness when an idempotency key is added.

Sagas

A saga coordinates a long-running workflow that may span multiple operations or external interactions.

Saga concerns are separated into distinct capabilities:

  • IBaseSaga<TId> provides identity and completion state.
  • IBaseSagaHandler<TEvent> handles a specific event type.
  • IBaseSagaCompensator<TError> provides explicit business compensation.
  • IBaseSagaRepository<TSaga, TId> persists saga state.

A saga may implement multiple IBaseSagaHandler<TEvent> interfaces.

Compensation represents explicit business actions that address previously completed workflow steps.

Compensation is not transactional rollback.


Abstractions

IBaseCommandHandler

Represents an application use case that handles caller intent.

public interface IBaseCommandHandler<in TCommand, TResult, TError>
{
    Task<Result<TResult, TError>> HandleAsync(
        TCommand command,
        CancellationToken ct = default);
}

Use Unit when the command has no meaningful success value.

public sealed class CreateOrderHandler
    : IBaseCommandHandler<CreateOrderCommand, Unit, CreateOrderError>
{
}

The handler coordinates application behavior.

Domain rules and invariants remain in the domain model.

IBaseQueryHandler

Represents an application-level read use case.

public interface IBaseQueryHandler<in TQuery, TResult, TError>
{
    Task<Result<TResult, TError>> HandleAsync(
        TQuery query,
        CancellationToken ct = default);
}

Expected failures are represented through TError.

Unexpected runtime or infrastructure failures propagate as exceptions.

IBaseRepository

Defines the minimal persistence contract for aggregate roots.

public interface IBaseRepository<TAggregate, TId>
{
    Task<Optional<TAggregate>> GetByIdAsync(
        TId id,
        CancellationToken ct = default);

    Task AddAsync(
        TAggregate aggregate,
        CancellationToken ct = default);

    Task UpdateAsync(
        TAggregate aggregate,
        CancellationToken ct = default);

    Task DeleteAsync(
        TAggregate aggregate,
        CancellationToken ct = default);
}

The operations express persistence intent. They do not imply that the current Unit of Work has been committed.

AddAsync is intended for a new aggregate.

UpdateAsync is intended for an existing aggregate.

DeleteAsync represents physical removal from persistence. It is distinct from a domain-level logical deletion, which is normally represented as a domain state transition followed by UpdateAsync.

Infrastructure implements this contract.

IUnitOfWork

Defines the transactional boundary for an application operation.

public interface IUnitOfWork
{
    Task BeginAsync(CancellationToken ct = default);

    Task CommitAsync(CancellationToken ct = default);

    Task RollbackAsync(CancellationToken ct = default);
}

Participating application-layer ports contribute work to the current Unit of Work.

CommitAsync commits that work according to the transaction mechanism provided by infrastructure.

RollbackAsync abandons uncommitted work.

The abstraction does not expose the underlying transaction technology.

IBaseIdempotencyStore

Coordinates idempotent processing.

public interface IBaseIdempotencyStore
{
    Task<bool> ExistsAsync(
        string key,
        CancellationToken ct = default);

    Task AddAsync(
        string key,
        CancellationToken ct = default);
}

ExistsAsync determines whether an idempotency key has already been recorded.

It must not be treated as a concurrency guarantee.

AddAsync participates in the current Unit of Work, and implementations must enforce uniqueness of the key.

The idempotency record becomes durable when the Unit of Work commits.

IIntegrationEvent

Marks a type as an Integration Event.

public interface IIntegrationEvent
{
}

The marker is intentionally empty.

Event identifiers, timestamps, correlation information, transport metadata, and serialization concerns are not imposed by this interface.

IBaseIntegrationEventOutbox

Adds Integration Events to a durable outbox.

public interface IBaseIntegrationEventOutbox
{
    Task AddAsync<TIntegrationEvent>(
        TIntegrationEvent integrationEvent,
        CancellationToken ct = default)
        where TIntegrationEvent : IIntegrationEvent;
}

Integration Events added through this port participate in the current Unit of Work.

They become durable when the Unit of Work commits.

Adding an Integration Event does not publish it.

IBaseIntegrationEventPublisher

Publishes Integration Events to external messaging infrastructure.

public interface IBaseIntegrationEventPublisher
{
    Task PublishAsync<TIntegrationEvent>(
        TIntegrationEvent integrationEvent,
        CancellationToken ct = default)
        where TIntegrationEvent : IIntegrationEvent;
}

This contract represents publication only.

It does not provide durable storage, transaction participation, outbox behavior, retry guarantees, delivery guarantees, or idempotent consumption.

Those concerns belong to the infrastructure implementation and surrounding architecture.


Saga Abstractions

IBaseSaga

Defines saga identity and completion state.

public interface IBaseSaga<out TId>
{
    TId Id { get; }

    bool IsCompleted { get; }
}

The identifier type is application-defined.

The package does not force sagas to use Guid.

IBaseSagaHandler

Defines the capability for a saga to handle a specific event type.

public interface IBaseSagaHandler<in TEvent>
{
    Task HandleAsync(
        TEvent evt,
        CancellationToken ct = default);
}

A saga that handles multiple event types implements multiple handler contracts.

public sealed class OrderSaga :
    IBaseSaga<OrderSagaId>,
    IBaseSagaHandler<OrderCreated>,
    IBaseSagaHandler<PaymentCompleted>
{
}

This avoids untyped object event dispatch in the saga contract.

IBaseSagaCompensator

Defines explicit compensation behavior for a failed saga workflow.

public interface IBaseSagaCompensator<in TError>
{
    Task CompensateAsync(
        TError error,
        CancellationToken ct = default);
}

TError describes the failure requiring compensation.

Compensation is an explicit business operation.

It must not be confused with transaction rollback.

IBaseSagaRepository

Defines persistence of saga state.

public interface IBaseSagaRepository<TSaga, TId>
    where TSaga : IBaseSaga<TId>
{
    Task<Optional<TSaga>> GetByIdAsync(
        TId id,
        CancellationToken ct = default);

    Task AddAsync(
        TSaga saga,
        CancellationToken ct = default);

    Task UpdateAsync(
        TSaga saga,
        CancellationToken ct = default);
}

Saga persistence is deliberately separate from aggregate persistence.

A saga represents workflow state, not an aggregate root.

The base contract does not define deletion. Retention and cleanup of completed saga state are separate lifecycle and operational concerns.


Extensions

Extensions are small orchestration helpers over the package contracts.

They are conveniences rather than mandatory execution mechanisms.

UnitOfWorkExtensions

UnitOfWorkExtensions.ExecuteAsync executes application logic within a Unit of Work.

await unitOfWork.ExecuteAsync(async ct =>
{
    // Application logic

    return Result.Success<Unit, Error>(Unit.Value);
});

Behavior:

  • begin the Unit of Work
  • execute the application logic
  • commit when the application logic succeeds
  • rollback when the application logic returns an expected failure
  • rollback and rethrow when the application logic throws

If application logic throws and rollback also throws, both failures are preserved in an AggregateException.

A failed CommitAsync is not automatically followed by rollback. Once commit has been attempted, the generic extension cannot safely assume that the underlying transaction remains rollbackable.

That responsibility belongs to the concrete Unit of Work implementation.

CommandHandlerExtensions

Executes a command handler within a Unit of Work.

await handler.ExecuteAsync(
    command,
    unitOfWork,
    ct);

The extension delegates transaction behavior to UnitOfWorkExtensions.

It does not duplicate transaction management or reinterpret the handler's Result.

QueryHandlerExtensions

Executes a query handler directly.

await handler.ExecuteAsync(
    query,
    ct);

The extension introduces no transaction management or additional application orchestration.


Transactional Outbox Pattern

A common use case combines aggregate persistence with Integration Event durability.

A typical flow is:

  1. Begin the Unit of Work.
  2. Add or update the aggregate.
  3. Add the Integration Event to the outbox.
  4. Commit the Unit of Work.
  5. Later, read the durable outbox entry.
  6. Publish it through IBaseIntegrationEventPublisher.

The important boundary is the commit.

The aggregate changes and outbox entry can participate in the same transactional boundary.

External message delivery occurs separately.

This prevents successful business persistence from depending on the immediate availability of an external message broker.


Idempotent Command Processing

A typical idempotent operation may:

  1. Check whether the idempotency key has already been recorded.
  2. Return the appropriate already-processed result when it exists.
  3. Perform the application operation when it does not exist.
  4. Stage the required persistence changes.
  5. Add the idempotency key within the same Unit of Work.
  6. Commit the Unit of Work.

The existence check is not sufficient to prevent concurrent duplicate processing.

The infrastructure implementation must enforce uniqueness when the key is added so that competing operations cannot both successfully commit the same idempotency key.


Error Handling

The package distinguishes expected and unexpected failures.

Expected failures are values:

Result<TResult, TError>

Examples include business rejection, invalid requested transitions, duplicate logical operations, and other failures the application deliberately models.

Unexpected failures remain exceptions.

Examples include database unavailability, serialization failures, network failures, and programming errors.

Applications decide which failures are expected in the context of each use case.

The package deliberately does not provide a universal ApplicationError base type.


Domain Facts

Optima.Net.Application does not define Domain Fact contracts or a Domain Fact outbox.

Domain Facts belong to the domain model and would require a dependency on domain-model abstractions.

Applications that use Domain Facts for auditing or other processing should define the appropriate application-layer ports in a package that explicitly depends on their domain model.

This keeps Optima.Net.Application usable without forcing an Optima.Net.DomainModel dependency.


Framework-Owned Contracts

Interfaces prefixed with IBase are framework-owned contracts.

Consumers may implement them directly or derive application-specific interfaces when that improves clarity.

For example:

public interface ICreateOrderHandler
    : IBaseCommandHandler<CreateOrderCommand, OrderId, CreateOrderError>
{
}

Application-specific interfaces should add meaning rather than simply duplicate framework contracts.


Infrastructure Boundary

Infrastructure implementations may use any appropriate technology.

For example, an IBaseRepository implementation might use SQL, DynamoDB, MongoDB, or another persistence technology.

An IUnitOfWork implementation might use a database transaction, a DynamoDB transactional write, or another atomic persistence mechanism.

An IBaseIntegrationEventOutbox implementation might use a relational outbox table, DynamoDB outbox items, or another durable store.

An IBaseIntegrationEventPublisher implementation might use Kafka, RabbitMQ, SNS, SQS, Azure Service Bus, or another messaging technology.

None of those technologies are exposed through the application contracts.


Final Notes

Optima.Net.Application is deliberately small.

Its job is to provide stable application-layer boundaries, not to become an application framework that owns every architectural decision.

The package favors:

  • explicit contracts
  • explicit transaction boundaries
  • typed commands and queries
  • typed Integration Events
  • typed saga event handling
  • clear separation between aggregate and saga persistence
  • expected failures as values
  • unexpected failures as exceptions
  • infrastructure behind application-layer ports

If an abstraction does not belong to the application layer or cannot be expressed without leaking an unrelated framework or infrastructure concern, it does not belong in this package.

For additional examples, see the Optima.Net.TestHarnesses project on GitHub when available.

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  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

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
4.0.1 105 10/20/2026
4.0.0 100 10/14/2026
3.0.1 94 10/13/2026
3.0.0 93 10/7/2026
2.0.0 148 6/23/2026
1.0.0 142 1/12/2026

RELEASENOTES.md