Optima.Net.Domain
3.0.1
Prefix Reserved
See the version list below for details.
dotnet add package Optima.Net.Domain --version 3.0.1
NuGet\Install-Package Optima.Net.Domain -Version 3.0.1
<PackageReference Include="Optima.Net.Domain" Version="3.0.1" />
<PackageVersion Include="Optima.Net.Domain" Version="3.0.1" />
<PackageReference Include="Optima.Net.Domain" />
paket add Optima.Net.Domain --version 3.0.1
#r "nuget: Optima.Net.Domain, 3.0.1"
#:package Optima.Net.Domain@3.0.1
#addin nuget:?package=Optima.Net.Domain&version=3.0.1
#tool nuget:?package=Optima.Net.Domain&version=3.0.1
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.
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.
public sealed class OrderDiscountPolicy : IPolicy<Order>
{
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
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 => nameof(PasswordPolicy);
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>
{
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 DiagnosticSpecificationEvaluator();
//evaluator.Evaluate return a PolicyDiagnosticResult
var diagnostic = evaluator.Evaluate(policies, password);
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 string PolicyName => nameof(CreditApprovalPolicy);
public bool IsSatisfiedBy(CreditApplication candidate)
=> true;
public PolicyFailureSemantics FailureSemantics
=> PolicyFailureSemantics.Replaceable;
}
public sealed class AmlClearancePolicy
: IPolicy<CreditApplication>, INamedPolicy
{
public string PolicyName => nameof(AmlClearancePolicy);
public bool IsSatisfiedBy(CreditApplication candidate)
=> true;
public PolicyFailureSemantics FailureSemantics
=> PolicyFailureSemantics.Terminal;
}
PolicyJustification
public sealed class CreditApprovalPolicyJustification
: IPolicyJustification<CreditApplication>
{
public string PolicyName => nameof(CreditApprovalPolicy);
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 => nameof(AmlClearancePolicy);
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);
Interpreting the result
if (diagnostic.Fulfilled)
{
// Credit may proceed
}
else
{
var failures = diagnostic.GetFailures();
// These are SPEC failures, not policy failures
// e.g. CreditWithinLimitSpec
if (diagnostic.Semantics.HasFlag(PolicyFailureSemantics.Replaceable))
{
// Offer alternative product
}
if (diagnostic.Semantics.HasFlag(PolicyFailureSemantics.Terminal))
{
// Stop immediately
}
}
Explicit showcasing of Policy.IsSatisfiedBy()
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
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);
public Task<bool> IsSatisfiedByAsync(
CreditApplication candidate,
CancellationToken ct = default)
=> Task.FromResult(true);
public PolicyFailureSemantics FailureSemantics
=> PolicyFailureSemantics.Replaceable;
}
//////////////////////////////////////////////////////////////////////////
public sealed class CreditApprovalPolicyJustification
: IAsyncPolicyJustification<CreditApplication>
{
public string PolicyName => nameof(CreditApprovalPolicy);
public IReadOnlyCollection<IAsyncSpecification<CreditApplication>> Specifications { get; }
public CreditApprovalPolicyJustification(
CreditWithinLimitSpec limitSpec,
NoPriorDefaultsSpec noDefaultsSpec)
{
Specifications = new IAsyncSpecification<CreditApplication>[]
{
limitSpec,
noDefaultsSpec
};
}
}
/////////////////////////////////////////////////////////////////////////////////////
var evaluator = new AsyncPolicyDiagnosticEvaluator(
new AsyncSpecificationEvaluator());
var diagnostic = await evaluator.EvaluateAllAsync(
new[]
{
(Policy: creditApprovalPolicy,
Justification: creditApprovalPolicyJustification)
},
creditApplication,
cancellationToken);
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
public interface IUseCase<in TRequest, out TResult>
{
TResult Execute(TRequest request);
}
Use cases:
- coordinate intent
- call ACLs and policies
- do not enforce business truth
Saga
public interface ISaga<in TInput>
{
void Execute(TInput input);
}
Sagas:
- own retries and compensation
- handle partial failure
- never return values
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