Optima.Net.Application 4.0.1

Prefix Reserved
dotnet add package Optima.Net.Application --version 4.0.1
                    
NuGet\Install-Package Optima.Net.Application -Version 4.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="4.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="4.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 4.0.1
                    
#r "nuget: Optima.Net.Application, 4.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@4.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=4.0.1
                    
Install as a Cake Addin
#tool nuget:?package=Optima.Net.Application&version=4.0.1
                    
Install as a Cake Tool

Optima.Net.Application

Overview

Optima.Net.Application supplies general-purpose application-layer contracts for the Optima.Net ecosystem. It can be used with or without Domain-Driven Design.

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 general object persistence and aggregate-aware persistence are 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 and Hexagonal Architecture. It supports Domain-Driven Design without requiring it.

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

This package:

  • defines command and query handler contracts
  • defines general-purpose object persistence and aggregate-aware 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 consuming applications or packages that explicitly depend on the domain model. Neither repository contract requires Optima.Net.DomainModel.

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.

General-Purpose and Aggregate-Aware Persistence

The package provides two distinct repository contracts.

IBaseRepository<TObject> supports generic object persistence. It does not require Domain-Driven Design, an aggregate root, or a native identifier on TObject.

IAggregateRepository<TEntity, TEntityId> supports an aggregate consistency boundary with expected ordinals. It is suitable when the consuming application needs ordinal-based optimistic concurrency.

The consuming application chooses the contract and implements it using its own domain knowledge and persistence technology. The package does not implement a repository or decide how objects are stored.

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.

Repository Contracts

Generic persistence is not automatically aggregate persistence. A consumer can retrieve or persist objects independently when that matches its application design. An aggregate-aware repository coordinates work against a single aggregate identity, even when it persists only an individual component.

The identifier supplied to a general-purpose repository method need not be an identifier belonging to the returned object. For example, a Company ID could be used to retrieve the addresses associated with that Company. The consumer decides and documents the valid identifier semantics.

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

IBaseRepository<TObject> is the general-purpose persistence contract. TObject may be an aggregate, an entity, a value object, or another application-defined type. No native object ID is required by the interface.

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

    Task<IReadOnlyList<TObject>> GetAllByIdAsync<TId>(
        TId id,
        CancellationToken ct = default);

    Task AddAsync(
        TObject obj,
        CancellationToken ct = default);

    Task UpdateAsync(
        TObject obj,
        CancellationToken ct = default);

    Task DeleteAsync(
        TObject obj,
        CancellationToken ct = default);
}

GetByIdAsync<TId> retrieves one object or returns an empty Optional<TObject> when it does not exist.

GetAllByIdAsync<TId> retrieves all objects associated with the supplied identifier and returns an empty collection when none exist.

TId is a method-level type parameter. The interface does not require every TObject to have a native ID and does not prescribe what an identifier means. The consuming developer must decide which identifier types and access patterns the implementation supports.

For example, an application may implement IBaseRepository<CompanyAddress> so that GetAllByIdAsync(companyId) retrieves the addresses belonging to that Company. CompanyAddress need not have an independent ID.

AddAsync, UpdateAsync, and DeleteAsync receive an object rather than a separate identifier. The implementation must have enough information to identify the persistence target. In particular, UpdateAsync and DeleteAsync cannot be implemented unambiguously if the supplied object does not identify the target through its own data or an explicitly defined mapping or context.

When additional identifiers or lookup criteria are required, the consuming application can define a specialized repository contract. The consumer is responsible for ensuring that the operations it exposes are meaningful and correctly implemented.

IBaseRepository<TObject> does not require an ordinal or enforce aggregate concurrency. Applications choosing it must provide any concurrency protection their use cases require.

Repository methods express persistence intent and may stage operations for a Unit of Work. They do not, by themselves, imply a successful commit.

IAggregateRepository

IAggregateRepository<TEntity, TEntityId> is the aggregate-aware repository contract. The generic name TEntity reflects that an aggregate root is an entity with a consistency boundary. TEntityId identifies that boundary.

public interface IAggregateRepository<TEntity, TEntityId>
{
    Task<Optional<(TEntity Entity, long LastOrdinal)>> GetByIdAsync(
        TEntityId entityId,
        CancellationToken ct = default);

    Task AddAsync<TObject>(
        TEntityId entityId,
        TObject obj,
        long expectedOrdinal,
        CancellationToken ct = default);

    Task UpdateAsync<TObject>(
        TEntityId entityId,
        TObject obj,
        long expectedOrdinal,
        CancellationToken ct = default);

    Task DeleteAsync<TObject>(
        TEntityId entityId,
        TObject obj,
        long expectedOrdinal,
        CancellationToken ct = default);
}

GetByIdAsync returns the aggregate root and its last committed ordinal, or an empty Optional when the aggregate does not exist.

AddAsync, UpdateAsync, and DeleteAsync accept any appropriate TObject associated with the aggregate. This permits the application to stage a specific entity or value object without forcing the infrastructure to rewrite the entire aggregate for each operation.

entityId is the aggregate identity used for consistency protection. expectedOrdinal is the ordinal observed for that aggregate when the caller established the state on which the operation is based. It is not an ordinal allocated for the new operation.

When a new aggregate has no audit history, the expected ordinal can be zero by convention. Infrastructure must still protect against inserting an aggregate or stream that already exists.

All operations staged for the same aggregate within one Unit of Work use the same original expected ordinal. The concrete implementation must enforce concurrency at commit time, atomically with the related persistence operations. The application-specific audit mechanism determines how any new ordinals are allocated and recorded.

IAggregateRepository does not inherit from IBaseRepository<TObject>. That separation avoids exposing unversioned update or delete operations through the aggregate-aware contract.

A specialized consumer contract may add explicit operations, such as persisting a Company address under its parent CompanyId and expected ordinal. Domain behavior must still enforce the aggregate's invariants before the operation is staged. The repository is responsible for persistence, not for deciding whether a business operation is allowed.

These repository contracts do not depend on Optima.Net.DomainModel. They are implemented by the consuming application's Infrastructure layer or another chosen persistence adapter.

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 =>
{
    // Execute application operations that participate in this Unit of Work.
    return Result<Unit, MyError>.Ok(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 application state persistence with Integration Event durability.

A typical flow is:

  1. Begin the Unit of Work.
  2. Stage application state changes through the appropriate repository.
  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 state changes and outbox entry should participate in the same atomic transaction when the use case requires that guarantee. The concrete Unit of Work and repository implementations are responsible for providing it.

External message delivery occurs separately.

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

Domain Fact auditing is a separate concern. An application that produces Domain Facts can define an application-specific outbox contract and stage those facts along with state changes. Neither IDomainFact nor IDomainFactOutbox is provided by Optima.Net.Application.

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. IAggregateRepository is also a framework-owned contract, with its name chosen to make its aggregate semantics explicit.

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, implementations of IBaseRepository<TObject> and IAggregateRepository<TEntity, TEntityId> might use PostgreSQL, SQL Server, Oracle, 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 focused on contracts and small orchestration helpers. It is suitable for applications that do not use Domain-Driven Design as well as applications that do.

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
  • aggregate-aware optimistic concurrency through expected ordinals
  • typed commands and queries
  • typed Integration Events
  • typed saga event handling
  • separate general-purpose object, aggregate-aware, and saga persistence contracts
  • 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