Optima.Net.Domain 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.Domain --version 3.0.1
                    
NuGet\Install-Package Optima.Net.Domain -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.Domain" 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.Domain" Version="3.0.1" />
                    
Directory.Packages.props
<PackageReference Include="Optima.Net.Domain" />
                    
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.Domain --version 3.0.1
                    
#r "nuget: Optima.Net.Domain, 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.Domain@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.Domain&version=3.0.1
                    
Install as a Cake Addin
#tool nuget:?package=Optima.Net.Domain&version=3.0.1
                    
Install as a Cake Tool

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

In many systems, business rules end up:

  • buried in nested if statements,
  • 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

  • SpecificationIs this fact true?

  • PolicyMay we proceed if the facts hold?

  • Policy JustificationWhich facts justify this policy?

  • DiagnosticsWhich 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 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • 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.

Version Downloads Last Updated
3.1.3 159 1/7/2026
3.1.2 147 12/29/2025
3.1.1 110 12/29/2025
3.1.0 111 12/27/2025
3.0.1 128 12/26/2025
3.0.0 150 12/26/2025
2.0.0 190 12/24/2025
1.0.2 194 12/23/2025
1.0.1 188 12/22/2025
1.0.0 189 12/22/2025

RELEASENOTES.md