Optima.Net.DomainModel
1.0.4
Prefix Reserved
dotnet add package Optima.Net.DomainModel --version 1.0.4
NuGet\Install-Package Optima.Net.DomainModel -Version 1.0.4
<PackageReference Include="Optima.Net.DomainModel" Version="1.0.4" />
<PackageVersion Include="Optima.Net.DomainModel" Version="1.0.4" />
<PackageReference Include="Optima.Net.DomainModel" />
paket add Optima.Net.DomainModel --version 1.0.4
#r "nuget: Optima.Net.DomainModel, 1.0.4"
#:package Optima.Net.DomainModel@1.0.4
#addin nuget:?package=Optima.Net.DomainModel&version=1.0.4
#tool nuget:?package=Optima.Net.DomainModel&version=1.0.4
Optima.Net.DomainModel
Introduction
Welcome to Optima.Net.DomainModel. This package defines the structural foundation of your domain. It ensures that your software model only allows valid and legal domain states.
Optima.Net.DomainModel defines what exists in the domain (entities, value objects, aggregates) and what must always be true (invariants). It does not manage workflows, persistence, or event dispatching.
This package depends on Optima.Net.Events, which is included automatically when you install this package. You can read more about it here:
https://www.nuget.org/packages/Optima.Net.Events
TL;DR (for people who do not want to read the README)
If you prefer to see DomainModel in action instead of reading about it,
you can clone and run the test harness here:
https://github.com/snamretsuek/Optima.Net.TestHarnesses
It contains console applications that:
- wire everything together correctly,
- show real execution flows,
- and let you step through the code in a debugger.
If you still have questions after running the harness, then yes, you'll need to come back and read this README. 😃
NB: The test harnesses are for demonstration purposes only. The test harness project contains test harnesses for multiple Optima.Net packages. You need to run the one that applies to Optima.Net.DomainModel.
Purpose
Every business has rules. For example:
- A customer must have a name.
- An order cannot have a negative total.
- A product must have a price.
Optima.Net.DomainModel makes those rules enforceable by code. It ensures that your domain objects cannot exist in an illegal state.
This library defines the shape and rules of your domain. It does not handle application logic or persistence.
Key Concepts
Entity
An Entity is something that has a unique identity. The identity is immutable and defines equality.
using System;
using Optima.Net.DomainModel.Entities;
public sealed class Customer : Entity<Guid>
{
public string Name { get; }
public Customer(Guid id, string name)
: base(id)
{
Name = name;
}
}
Usage example:
var c1 = new Customer(Guid.NewGuid(), "Alice");
var c2 = new Customer(c1.Id, "Alice Clone");
Console.WriteLine(c1 == c2); // True - same identity
Value Object
A Value Object is defined by its contents, not by identity.
public record Money(decimal Amount, string Currency);
Usage example:
var a = new Money(100, "USD");
var b = new Money(100, "USD");
Console.WriteLine(a == b); // True
Invariant
An Invariant is a rule that must always be true. If it is violated, an exception is thrown immediately.
using Optima.Net.DomainModel.Invariants;
Invariant.MustBeTrue(total > 0, "Order total must be positive.");
If this rule fails, an InvariantViolationException will be thrown, stopping the system from continuing in an invalid state.
Enforcing Invariants (Optional Integration with Optima.Net.Domain)
By design, Optima.Net.DomainModel defines pure, immutable domain entities and value objects.
It does not perform invariant checking, validation, or policy enforcement within the model itself.
This is deliberate � invariants belong to the domain layer, not the model.
However, developers who wish to enforce aggregate-level or entity-level invariants can optionally integrate with the Optima.Net.Domain package.
Optima.Net.Domain provides a declarative policy and specification framework that allows you to express
and evaluate invariants without introducing procedural logic inside your models.
Example Integration
public sealed class Customer : Entity<CustomerId>
{
public string Email { get; }
public DateOnly DateOfBirth { get; }
public Customer(CustomerId id, string email, DateOnly dob)
: base(id)
{
Email = email;
DateOfBirth = dob;
// Optionally enforce invariants using Optima.Net.Domain
var policy = new CustomerInvariantsPolicy();
var justification = new CustomerInvariantsJustification();
var evaluator = new PolicyDiagnosticEvaluator(new SpecificationEvaluator());
var result = evaluator.Evaluate(policy, justification, this);
if (!result.Fulfilled)
{
throw new DomainModelInvariantViolationException(result);
}
}
}
In this example:
CustomerInvariantsPolicydefines what invariants must hold true (for example, age limits, email validity).CustomerInvariantsJustificationlists the underlying specifications for those invariants.PolicyDiagnosticEvaluator(fromOptima.Net.Domain) evaluates them deterministically.- If any invariant fails, the model constructor rejects invalid state.
This pattern provides explicit, explainable, and testable invariants without embedding business logic directly in your domain entities.
Key Points
- Invariants remain declarative and auditable.
- Evaluation logic lives outside the model, preserving purity.
- Integrating with
Optima.Net.Domainis optional � the model package itself has no dependency on it.
Domain Facts (GenericEvent and DynamicPayload)
Domain facts describe what has happened in your system. They are represented by GenericEvent and DynamicPayload from Optima.Net.Events.
GenericEventrepresents an immutable domain fact.DynamicPayloadholds the data for that fact.
Example:
using Optima.Net.Events.Payloads;
using Optima.Net.Events.Models;
var payload = new DynamicPayload("OrderCreated");
payload.Add("OrderId", Guid.NewGuid());
payload.Add("TotalAmount", 250.00m);
var evt = new GenericEvent<DynamicPayload>
{
EventId = Guid.NewGuid(),
EventType = payload.PayloadName,
Source = "OrderAggregate",
SchemaVersion = "V1.0.0",
Timestamp = DateTime.UtcNow,
Payload = payload
};
This represents the domain fact: "OrderCreated occurred with these values."
Access the payload fields like this:
var orderId = evt.Payload["OrderId"];
var total = evt.Payload["TotalAmount"];
For a deeper explanation of these types, see the readme for Optima.Net.Events:
https://www.nuget.org/packages/Optima.Net.Events
Aggregate Root Example
An Aggregate Root defines a consistency boundary. It enforces invariants and emits domain events. It does not handle or dispatch them.
using System;
using Optima.Net.DomainModel.Entities;
using Optima.Net.DomainModel.Invariants;
using Optima.Net.Events;
using Optima.Net.Events.Payloads;
public sealed class Order : AggregateRoot<Guid>
{
public string OrderNumber { get; }
public decimal TotalAmount { get; private set; }
private Order(Guid id, string orderNumber, decimal totalAmount)
: base(id)
{
Invariant.MustBeTrue(!string.IsNullOrWhiteSpace(orderNumber), "Order number must not be empty.");
Invariant.MustBeTrue(totalAmount > 0, "Order total must be greater than zero.");
OrderNumber = orderNumber;
TotalAmount = totalAmount;
EmitOrderCreatedEvent();
}
public static Order Create(Guid id, string orderNumber, decimal totalAmount)
{
return new Order(id, orderNumber, totalAmount);
}
public void UpdateTotal(decimal newAmount)
{
Invariant.MustBeTrue(newAmount > 0, "Order total must be greater than zero.");
TotalAmount = newAmount;
EmitOrderUpdatedEvent();
}
private void EmitOrderCreatedEvent()
{
var payload = new DynamicPayload("OrderCreated");
payload.Add("OrderId", Id);
payload.Add("OrderNumber", OrderNumber);
payload.Add("TotalAmount", TotalAmount);
payload.Add("CreatedAtUtc", DateTime.UtcNow);
var evt = new GenericEvent<DynamicPayload>
{
EventId = Guid.NewGuid(),
EventType = payload.PayloadName,
Source = nameof(Order),
SchemaVersion = "V1.0.0",
Timestamp = DateTime.UtcNow,
Payload = payload
};
EmitDomainFact(evt);
}
private void EmitOrderUpdatedEvent()
{
var payload = new DynamicPayload("OrderUpdated");
payload.Add("OrderId", Id);
payload.Add("OrderNumber", OrderNumber);
payload.Add("TotalAmount", TotalAmount);
payload.Add("UpdatedAtUtc", DateTime.UtcNow);
var evt = new GenericEvent<DynamicPayload>
{
EventId = Guid.NewGuid(),
EventType = payload.PayloadName,
Source = nameof(Order),
SchemaVersion = "V1.0.0",
Timestamp = DateTime.UtcNow,
Payload = payload
};
EmitDomainFact(evt);
}
}
Usage example:
var order = Order.Create(Guid.NewGuid(), "ORD-1001", 250.00m);
order.UpdateTotal(300.00m);
foreach (var fact in order.DomainFacts)
{
Console.WriteLine($"{fact.EventType} emitted at {fact.Timestamp:u}");
}
Common Mistakes and Why They Are Wrong
| Mistake | Why It Is Wrong |
|---|---|
| Returning diagnostics from invariants | Invariants are binary: valid or illegal. |
| Letting aggregates handle or dispatch events | Aggregates may emit domain events, but they must never dispatch or react to them. |
| Including persistence logic | DomainModel should not depend on infrastructure or data storage. |
| Using Optional or Result for invariants | Invalid states must throw exceptions immediately. |
Example Output
OrderCreated emitted at 2026-01-04 18:12:03Z
OrderUpdated emitted at 2026-01-04 18:12:05Z
Summary
Optima.Net.DomainModel defines the legal structure and boundaries of your domain.
It:
- Prevents illegal states.
- Enforces invariants.
- Emits domain events.
- Includes the Optima.Net.Events dependency automatically.
- Works immediately after installation without any setup.
Each Optima package is designed to work independently, but you can explore other packages in the Optima.Net ecosystem if you wish to expand your domain-driven architecture.
License
This project is licensed under the MIT License.
| 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
- Optima.Net (>= 1.0.9)
- Optima.Net.Events (>= 1.0.5)
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