Optima.Net.Domain
3.1.3
Prefix Reserved
dotnet add package Optima.Net.Domain --version 3.1.3
NuGet\Install-Package Optima.Net.Domain -Version 3.1.3
<PackageReference Include="Optima.Net.Domain" Version="3.1.3" />
<PackageVersion Include="Optima.Net.Domain" Version="3.1.3" />
<PackageReference Include="Optima.Net.Domain" />
paket add Optima.Net.Domain --version 3.1.3
#r "nuget: Optima.Net.Domain, 3.1.3"
#:package Optima.Net.Domain@3.1.3
#addin nuget:?package=Optima.Net.Domain&version=3.1.3
#tool nuget:?package=Optima.Net.Domain&version=3.1.3
Optima.Net.Domain
Optima.Net.Domain is a lightweight, framework-agnostic toolkit for expressing business rules, decisions, and outcomes in a clear, auditable, and scalable Domain-Driven Design (DDD) style.
As we all know Events are a big part of DDD and you can find support for events in Optima.Net.Events (separate NuGet package) If you do not like dealing with nulls then check out Optima.Net (separate NuGet package) to Implement a light version of Optional<T>, Result<T> and Result<T, TError>
It does not try to be a framework. Instead, it provides a small, explicit set of primitives that scale from simple business rules to finance-grade decision systems.
This document is both:
- a comprehensive reference, and
- a guided tutorial for engineers new to decision-centric domain modelling.
If you read it top-to-bottom, you should be able to confidently design
Specifications, Policies, Diagnostics, and Application flows without
burying business logic in if statements.
Table of Contents
- Motivation and Mental Model
- Core Concepts Overview
- Specifications
- IPolicyJustification
- Policies
- Policy Failure Semantics
- Evaluating Policies
- Diagnostics and Failure Projection
- End-to-End Example: Password Policy
- End-to-End Example: Credit Approval
- Advanced Usage Patterns
- Anti-Corruption Layers (ACL)
- Application Layer: Use Cases and Sagas
- Design Philosophy and Boundaries
Motivation and Mental Model
In many systems, business rules end up:
- buried in nested
ifstatements, - duplicated across services,
- tightly coupled to infrastructure,
- or impossible to explain after the fact.
Optima.Net.Domain exists to fix this by making rules and decisions explicit.
Mental Model
| Concept | Purpose | Question Answered |
|---|---|---|
| Specification | Fact | Is this true? |
| Policy | Permission | May we proceed? |
| Diagnostics | Explanation | Why did this fail? |
| Application | Flow | What should happen next? |
Each concept has one job.
Core Concepts Overview
Specifications
- Express facts
- Pure and side-effect free
- Independently testable
- Composable
Policies
Policies no longer construct failures or return evaluation results.
- Express business intent
- Declare whether progression is allowed, and
- Declare what is permitted after failure via
PolicyFailureSemantics. - Do not orchestrate
Evaluation, diagnostics, and failure projection are performed externally by a PolicyDiagnosticEvaluator.
Diagnostics
- Observe evaluation
- Immutable and structured
- Explain failures without changing behaviour
Application Layer
- Coordinates flow
- Owns retries, compensation, escalation
- Never enforces business truth
Specifications
A specification answers one factual question.
Example Domain: Orders
public sealed class Order
{
private readonly List<OrderLine> _lines = new();
public IReadOnlyCollection<OrderLine> Lines => _lines;
public decimal Total => _lines.Sum(l => l.Price);
public void AddLine(OrderLine line) => _lines.Add(line);
}
//if the discountThreshold were set in stone then you could do this
public sealed class OrderDiscountSpec : ISpecification<Order>
{
private const decimal DiscountThreshold = 500m;
public bool IsSatisfiedBy(Order order)
=> order.Total > DiscountThreshold;
}
//if the discountThreshold is variable then you can do this
public sealed record OrderDiscountSpec(decimal DiscountThreshold)
: ISpecification<Order>
{
public OrderDiscountSpec(decimal discountThreshold)
: this(Validate(discountThreshold))
{
}
public bool IsSatisfiedBy(Order order)
=> order.Total > DiscountThreshold;
private static decimal Validate(decimal value)
{
if (value <= 0)
throw new ArgumentOutOfRangeException(
nameof(value),
"Discount threshold must be greater than zero.");
return value;
}
}
Each specification:
- answers one question
- has no business intent
- has no side effects
IPolicyJustification
A Policy Justification defines which facts must hold for a specific policy
to be considered satisfied.
It exists to answer a single question:
“Why is this policy satisfied or not?”
A policy justification:
belongs to the domain
is defined per policy
lists the specifications that justify that policy
contains no evaluation logic
contains no business intent
The evaluator uses a policy justification to:
evaluate the required facts,
bind those facts to a specific policy,
produce diagnostics that explain which facts failed.
Policies themselves:
do not evaluate specifications,
do not construct failures,
do not explain outcomes.
They only declare:
whether progression is allowed, and
what is permitted after failure via
PolicyFailureSemantics.
Mental model
Specification → Is this fact true?
Policy → May we proceed if the facts hold?
Policy Justification → Which facts justify this policy?
Diagnostics → Which facts failed, and why?
public sealed class OrderDiscountPolicyJustification: IPolicyJustification<Order>
{
public string PolicyName => nameof(OrderDiscountPolicy);
public IReadOnlyCollection<ISpecification<Order>> Specifications { get; }
public OrderDiscountPolicyJustification(
OrderDiscountSpec orderDiscountSpec)
{
Specifications = new ISpecification<Order>[]
{
orderDiscountSpec
};
}
}
Design rules
There is exactly one policy justification per policy.
Specifications may be reused across justifications.
A justification must match the policy it justifies.
The evaluator enforces this contract at runtime.
Policies
A policy declares whether progression is allowed.
Note on IsSatisfiedBy
IsSatisfiedByexists as a coarse compatibility check and for simple gating scenarios.- It must not be used to explain why a policy failed.
- All meaningful policy evaluation in Optima.Net.Domain is performed via
PolicyDiagnosticEvaluator, which produces immutable diagnostics.
public sealed class OrderDiscountPolicy : IPolicy<Order>, INamedPolicy
{
public const string PolicyId = "OrderDiscountPolicy";
public string PolicyName => PolicyId;
public bool IsSatisfiedBy(Order order)
=> true;
public PolicyFailureSemantics FailureSemantics
=> PolicyFailureSemantics.Correctable;
}
Policies:
- do not return results
- do not create failures
- do not orchestrate
They declare intent only.
Policy Failure Semantics
Failure semantics describe what is allowed after a failure, not whether something failed.
[Flags]
public enum PolicyFailureSemantics
{
Terminal = 0,
Correctable = 1 << 0,
Replaceable = 1 << 1
}
Examples:
- Terminal → stop immediately if failed
- Correctable → user may fix input
- Replaceable → alternative intent may be proposed
Evaluating Policies
Policies are evaluated using a PolicyDiagnosticEvaluator.
var evaluator = new PolicyDiagnosticEvaluator();
var diagnostic = evaluator.EvaluateAll(
OrderPolicies.All,
order);
Policies are evaluated as collections, not composites.
Diagnostics and Failure Projection
Evaluation produces a diagnostic tree.
var failures = diagnostic.GetFailures();
Rules:
- only failed leaf policies are projected
- parent/group nodes are never failures
- failures are immutable facts
A leaf represents the lowest-level diagnostic node. In the current model, specifications are leaf nodes, but consumers must not assume that failures always correspond to specifications.
End-to-End Example: Password Policy
Specifications:
public sealed class PasswordLengthSpec : ISpecification<string>
{
public bool IsSatisfiedBy(string password)
=> password.Length >= 12;
}
public sealed record PasswordEntropySpec : ISpecification<string>
{
private const int MinimumEntropyBits = 60;
public bool IsSatisfiedBy(string password)
{
if (string.IsNullOrEmpty(password))
return false;
var entropy = CalculateEntropy(password);
return entropy >= MinimumEntropyBits;
}
private static double CalculateEntropy(string password)
{
var distinctChars = password.Distinct().Count();
if (distinctChars == 0)
return 0;
// Shannon-style approximation:
// entropy ≈ length × log2(character set size)
return password.Length * Math.Log2(distinctChars);
}
}
PolicyJustification:
public sealed class PasswordPolicyJustification
: IPolicyJustification<string>
{
public string PolicyName => PasswordPolicy.PolicyId;
public IReadOnlyCollection<ISpecification<string>> Specifications { get; }
public PasswordPolicyJustification(
PasswordLengthSpec passwordLengthSpec,
PasswordEntropySpec passwordEntropySpec)
{
Specifications = new ISpecification<string>[]
{
passwordLengthSpec,
passwordEntropySpec
};
}
}
Policy
public sealed class PasswordPolicy : IPolicy<string>, INamedPolicy
{
public const string PolicyId = "PasswordPolicy ";
public string PolicyName => PolicyId;
public bool IsSatisfiedBy(string candidate)
=> true;
public PolicyFailureSemantics FailureSemantics
=> PolicyFailureSemantics.Correctable | PolicyFailureSemantics.Replaceable;
}
Policy Evaluation
var password= "UnderstoodEventually";
var justification = new PasswordPolicyJustification(
lengthSpec,
entropySpec);
//pair policies with justfications
var policies = new[]
{
(Policy: (IPolicy<string>)policy,
Justification: (IPolicyJustification<string>)justification)
};
var evaluator = new PolicyDiagnosticEvaluator(new SpecificationEvaluator());
var diagnostic = evaluator.EvaluateAll(policies, password);
PolicyDiagnosticEvaluator is the sole entry point for producing diagnostics. Specification-level evaluators are internal implementation details and are not used directly by consumers.
End-to-End Example: Credit Approval
The Domain Model
public sealed class CreditApplication
{
public decimal Amount { get; }
public bool HasPriorDefaults { get; }
public bool PassedAmlChecks { get; }
public CreditApplication(
decimal amount,
bool hasPriorDefaults,
bool passedAmlChecks)
{
Amount = amount;
HasPriorDefaults = hasPriorDefaults;
PassedAmlChecks = passedAmlChecks;
}
}
Specifications
public sealed record CreditWithinLimitSpec(decimal MaxAmount)
: ISpecification<CreditApplication>
{
public bool IsSatisfiedBy(CreditApplication app)
=> app.Amount <= 1000000;
}
public sealed record NoPriorDefaultsSpec
: ISpecification<CreditApplication>
{
public bool IsSatisfiedBy(CreditApplication app)
=> !app.HasPriorDefaults;
}
public sealed record PassedAmlChecksSpec
: ISpecification<CreditApplication>
{
public bool IsSatisfiedBy(CreditApplication app)
=> app.PassedAmlChecks;
}
Policies
public sealed class CreditApprovalPolicy
: IPolicy<CreditApplication>, INamedPolicy
{
public const string PolicyId = "CreditApprovalPolicy";
public string PolicyName => PolicyId;
public bool IsSatisfiedBy(CreditApplication candidate)
=> true;
public PolicyFailureSemantics FailureSemantics
=> PolicyFailureSemantics.Replaceable;
}
public sealed class AmlClearancePolicy
: IPolicy<CreditApplication>, INamedPolicy
{
public const string PolicyId = "AmlClearancePolicy";
public string PolicyName => PolicyId;
public bool IsSatisfiedBy(CreditApplication candidate)
=> true;
public PolicyFailureSemantics FailureSemantics
=> PolicyFailureSemantics.Terminal;
}
PolicyJustification
public sealed class CreditApprovalPolicyJustification
: IPolicyJustification<CreditApplication>
{
public string PolicyName => CreditApprovalPolicy.PolicyId;
public IReadOnlyCollection<ISpecification<CreditApplication>> Specifications { get; }
public CreditApprovalPolicyJustification()
{
Specifications = new ISpecification<CreditApplication>[]
{
new CreditWithinLimitSpec(100_000m),
new NoPriorDefaultsSpec()
};
}
}
public sealed class AmlClearancePolicyJustification
: IPolicyJustification<CreditApplication>
{
public string PolicyName => AmlClearancePolicy.PolicyId;
public IReadOnlyCollection<ISpecification<CreditApplication>> Specifications { get; }
public AmlClearancePolicyJustification()
{
Specifications = new ISpecification<CreditApplication>[]
{
new PassedAmlChecksSpec()
};
}
}
Evaluate the Policies
var application = new CreditApplication(
amount: 120_000m,
hasPriorDefaults: false,
passedAmlChecks: true);
// Build policies
var creditPolicy = new CreditApprovalPolicy();
var amlPolicy = new AmlClearancePolicy();
// Build justifications
var creditJustification =
new CreditApprovalPolicyJustification();
var amlJustification = new AmlClearancePolicyJustification();
// Pair policies with justifications
var policies = new[]
{
(Policy: (IPolicy<CreditApplication>)creditPolicy,
Justification: (IPolicyJustification<CreditApplication>)creditJustification),
(Policy: (IPolicy<CreditApplication>)amlPolicy,
Justification: (IPolicyJustification<CreditApplication>)amlJustification)
};
// Evaluate
var evaluator = new PolicyDiagnosticEvaluator(
new SpecificationEvaluator());
var diagnostic = evaluator.EvaluateAll(
policies,
application);
This is the authoritative evaluation path. Boolean policy checks should not be used for decision explanation.
Interpreting the result
if (diagnostic.Fulfilled)
{
// Credit may proceed
}
else
{
var failures = diagnostics.GetFailures();
// These are failed *leaf* diagnostics
// (currently specifications, but not assumed to be forever)
if (diagnostic.Semantics.HasFlag(PolicyFailureSemantics.Replaceable))
{
// Offer alternative product
}
if (diagnostic.Semantics.HasFlag(PolicyFailureSemantics.Terminal))
{
// Stop immediately
}
}
Compatibility-level usage of `IsSatisfiedBy
// Compatibility-level check only.
// Real decision-making must use diagnostics.
bool creditPolicyAllowsProgression =
creditPolicy.IsSatisfiedBy(application);
bool amlPolicyAllowsProgression =
amlPolicy.IsSatisfiedBy(application);
Meaning:
If credit approval fails, an alternative product may be offered.
Advanced Usage Patterns
Multiple semantics
PolicyFailureSemantics.Correctable | PolicyFailureSemantics.Replaceable
Async Variation
Async policy support
Optima.Net.Domain currently provides
IAsyncPolicy<T>for permission checks that depend on asynchronous facts.Full asynchronous policy diagnostics (including async justifications and evaluators) are intentionally not part of the current release and may be introduced in a future version if sufficient demand exists.
public interface ICreditConfigProvider
{
Task<decimal> GetMaxCreditAmountAsync(
CancellationToken ct = default);
}
///////////////////////////////////////////////
public sealed class CreditWithinLimitSpec
: IAsyncSpecification<CreditApplication>
{
private readonly ICreditConfigProvider _config;
public CreditWithinLimitSpec(ICreditConfigProvider config)
{
_config = config ?? throw new ArgumentNullException(nameof(config));
}
public async Task<bool> IsSatisfiedByAsync(
CreditApplication application,
CancellationToken ct = default)
{
var maxAmount = await _config.GetMaxCreditAmountAsync(ct);
return application.Amount <= maxAmount;
}
}
//////////////////////////////////////////////////////////////////////////
public sealed class NoPriorDefaultsSpec
: IAsyncSpecification<CreditApplication>
{
public Task<bool> IsSatisfiedByAsync(
CreditApplication app,
CancellationToken ct = default)
=> Task.FromResult(!app.HasPriorDefaults);
}
/////////////////////////////////////////////////////////////////////////
public sealed class CreditApprovalPolicy
: IAsyncPolicy<CreditApplication>, INamedPolicy
{
public string PolicyName => nameof(CreditApprovalPolicy);
//## Why diagnostics don’t apply here (by design)
//You cannot reliably explain:
// why a credit bureau returned a score
// why a fraud engine said “no”
// why a regulator blocked something
// Trying to force diagnostics here would be lying.
public async Task<bool> IsSatisfiedByAsync(CreditApplication application,
CancellationToken ct = default)
{
var score = await _riskService.GetRiskScoreAsync(
application.CustomerId,
ct);
return score <= 650;
}
public PolicyFailureSemantics FailureSemantics
=> PolicyFailureSemantics.Replaceable;
}
//////////////////////////////////////////////////////////////////////////
CreditApprovalPolicy policy = new CreditApprovalPolicy(); ;
`var allowed = await _policy.IsSatisfiedByAsync(application, ct);
if (!allowed)
{
// No diagnostics here — this is a permission gate
return ApplicationResult.Rejected(
"Credit approval policy not satisfied");
}
// Proceed
return ApplicationResult.Completed();``
Async diagnostics mirror the synchronous policy model but are optional and additive.
Most domains resolve asynchronous facts (databases, services, configuration) before policy evaluation and then evaluate policies synchronously.
Optima.Net.Domain provides async policy and specification primitives for cases where end-to-end async evaluation is required, but it does not mandate diagnostic symmetry.
Why does an async policy contain a predicate?
An
IAsyncPolicy<T>may contain inline boolean logic that resembles a specification. This is intentional.The predicate is not a domain fact. It is an interpretation of an asynchronous, external signal (e.g. fraud engines, regulators, credit bureaus) and is scoped strictly to the policy.
Because these facts are:
- external,
- volatile,
- non-deterministic, and
- not explainable in-domain,
they must not be modeled as specifications.
Async policies therefore act as permission gates, not diagnostic participants.
Anti-Corruption Layers (ACL)
An ACL protects the domain from foreign models and semantics.
Inbound ACLs translate meaning, not data.
public interface IInboundTranslator<in TForeign, TDomain>
{
TranslationResult<TDomain> Translate(TForeign input);
}
ACLs:
- absorb versioning
- reject ambiguity
- protect invariants
Application Layer: Use Cases and Sagas
Use Case
A Use Case represents a single, synchronous application intent.
Characteristics:
single entry point
deterministic
no retries
no compensation
completes or rejects immediately
Mental model
“Given this input, do we proceed or not?”
Minimal Contract
namespace Optima.Net.Domain.Application;
public interface IUseCase<in TRequest, out TResult>
{
TResult Execute(TRequest request);
}
There is no async variant by default. Async is a transport concern, not a modeling concern.
Realistic Example – Create Disbursement Instruction
public sealed class CreateDisbursementInstructionUseCase
: IUseCase<CreateDisbursementRequest, ApplicationResult>
{
private readonly IInboundTranslator<CreateDisbursementDto, DisbursementInstruction> _translator;
private readonly PolicyDiagnosticEvaluator _policyEvaluator;
private readonly IReadOnlyCollection<
(IPolicy<DisbursementInstruction> Policy,
IPolicyJustification<DisbursementInstruction> Justification)
> _policies;
private readonly DisbursementRepository _repository;
public CreateDisbursementInstructionUseCase(
IInboundTranslator<CreateDisbursementDto, DisbursementInstruction> translator,
PolicyDiagnosticEvaluator policyEvaluator,
IReadOnlyCollection<
(IPolicy<DisbursementInstruction>,
IPolicyJustification<DisbursementInstruction>)
> policies,
DisbursementRepository repository)
{
_translator = translator;
_policyEvaluator = policyEvaluator;
_policies = policies;
_repository = repository;
}
public ApplicationResult Execute(CreateDisbursementRequest request)
{
// Step 1 — Translate meaning (ACL)
var translation = _translator.Translate(request.Payload);
if (!translation.IsSuccess)
return ApplicationResult.Rejected(translation.Issues);
var instruction = translation.Value!;
// Step 2 — Ask the domain if we may proceed
var diagnostics =
_policyEvaluator.EvaluateAll(_policies, instruction);
if (!diagnostics.Fulfilled)
{
var failures = diagnostics.GetFailures();
return ApplicationResult.Rejected(failures);
}
// Step 3 — Persist intent
_repository.Save(instruction);
return ApplicationResult.Completed();
}
}
Notice the following Key observations:
No business rules here
No IO except through collaborators
No retries
No workflow branching
Pure orchestration
Saga
Please take note that policies, and specifications as explained above should absolutely be used with a Saga. For brevity however I'm only showing the saga paradigm
A Saga represents a multi-step or failure-aware process.
Characteristics:
multiple steps
may span time
owns failure semantics
may retry, compensate, escalate, or stop
does not return a value
Mental model
“How do we try to make this happen safely?”
Minimal Contract
namespace Optima.Net.Domain.Application;
public interface ISaga<in TInput>
{
void Execute(TInput input);
}
If a Saga returns a value, it is lying. Outcomes are observed via state, decisions, or events.
Realistic Example – Execute Disbursement Saga
public sealed class ExecuteDisbursementSaga
: ISaga<DisbursementInstruction>, ICompensatingSaga
{
private readonly PolicyDiagnosticEvaluator _policyEvaluator;
private readonly IReadOnlyCollection<
(IPolicy<DisbursementInstruction> Policy,
IPolicyJustification<DisbursementInstruction> Justification)
> _policies;
private readonly PaymentsEngineGateway _payments;
private readonly DisbursementRepository _repository;
public ExecuteDisbursementSaga(
PolicyDiagnosticEvaluator policyEvaluator,
IReadOnlyCollection<
(IPolicy<DisbursementInstruction>,
IPolicyJustification<DisbursementInstruction>)
> policies,
PaymentsEngineGateway payments,
DisbursementRepository repository)
{
_policyEvaluator = policyEvaluator;
_policies = policies;
_payments = payments;
_repository = repository;
}
public void Execute(DisbursementInstruction instruction)
{
// STEP 0 — Ask the domain if execution may proceed
var diagnostics =
_policyEvaluator.EvaluateAll(_policies, instruction);
if (!diagnostics.Fulfilled)
{
instruction.MarkRejected(diagnostics.GetFailures());
_repository.Save(instruction);
return;
}
try
{
// STEP 1 — Mark intent
instruction.MarkInProgress();
// STEP 2 — Execute side effects
foreach (var line in instruction.Lines)
{
_payments.Execute(line);
instruction.MarkLineCompleted(line);
}
// STEP 3 — Complete
instruction.MarkCompleted();
_repository.Save(instruction);
}
catch (Exception ex)
{
Compensate(instruction, ex);
throw;
}
}
private void Compensate(
DisbursementInstruction instruction,
Exception exception)
{
instruction.MarkFailed(exception.Message);
_repository.Save(instruction);
// Best-effort rollback / escalation
// No assumption of full reversibility
}
}
Key observations:
Compensation is explicit
Compensation is contextual
No fake symmetry
The Saga owns failure semantics
Infrastructure is called, but not owned
** Compensating Saga Marker**
Some Sagas include explicit compensation logic.
To make that intent visible and enforceable, Optima provides a marker interface.
public interface ICompensatingSaga { }
Design Rule
If a Saga contains compensation or reversal logic, it must implement ICompensatingSaga.
This is:
a semantic signal
enforceable via code review or analyzers
intentionally behavior-free
There is no Compensate() method on the interface. Compensation is designed explicitly, not abstracted.
Design Philosophy and Boundaries
Optima.Net.Domain intentionally avoids:
- rule engines
- workflow engines
- infrastructure concerns
Its purpose is singular:
Make domain intent explicit, enforceable, and auditable.
If a rule matters, model it as a specification.
If a decision matters, model it as a policy.
If integration matters, protect it with an ACL.
| Product | Versions 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. |
-
net8.0
- No dependencies.
NuGet packages (1)
Showing the top 1 NuGet packages that depend on Optima.Net.Domain:
| Package | Downloads |
|---|---|
|
Optima.Net.NegotiatR
NegotiatR is a deterministic, single-pass negotiation engine that proposes alternative intents when policies fail. It consumes policy diagnostics, never re-evaluates rules, and guarantees outcomes. Designed for domain-driven systems, it keeps fallback decisions explicit, auditable, and separate from policy evaluation, workflows, and execution logic, without retries or implicit control flow. |
GitHub repositories
This package is not used by any popular GitHub repositories.
RELEASENOTES.md