Optima.Net.Application
3.0.0
Prefix Reserved
See the version list below for details.
dotnet add package Optima.Net.Application --version 3.0.0
NuGet\Install-Package Optima.Net.Application -Version 3.0.0
<PackageReference Include="Optima.Net.Application" Version="3.0.0" />
<PackageVersion Include="Optima.Net.Application" Version="3.0.0" />
<PackageReference Include="Optima.Net.Application" />
paket add Optima.Net.Application --version 3.0.0
#r "nuget: Optima.Net.Application, 3.0.0"
#:package Optima.Net.Application@3.0.0
#addin nuget:?package=Optima.Net.Application&version=3.0.0
#tool nuget:?package=Optima.Net.Application&version=3.0.0
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:
CreateOrderCancelOrderApprovePayment
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:
IBaseIntegrationEventOutboxprovides durable responsibility for later publication.IBaseIntegrationEventPublisherprovides 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:
- Begin the Unit of Work.
- Add or update the aggregate.
- Add the Integration Event to the outbox.
- Commit the Unit of Work.
- Later, read the durable outbox entry.
- 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:
- Check whether the idempotency key has already been recorded.
- Return the appropriate already-processed result when it exists.
- Perform the application operation when it does not exist.
- Stage the required persistence changes.
- Add the idempotency key within the same Unit of Work.
- 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 | Versions 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. |
-
net10.0
- Optima.Net (>= 2.0.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
RELEASENOTES.md