Optima.Net.DomainModel
1.0.0
Prefix Reserved
See the version list below for details.
dotnet add package Optima.Net.DomainModel --version 1.0.0
NuGet\Install-Package Optima.Net.DomainModel -Version 1.0.0
<PackageReference Include="Optima.Net.DomainModel" Version="1.0.0" />
<PackageVersion Include="Optima.Net.DomainModel" Version="1.0.0" />
<PackageReference Include="Optima.Net.DomainModel" />
paket add Optima.Net.DomainModel --version 1.0.0
#r "nuget: Optima.Net.DomainModel, 1.0.0"
#:package Optima.Net.DomainModel@1.0.0
#addin nuget:?package=Optima.Net.DomainModel&version=1.0.0
#tool nuget:?package=Optima.Net.DomainModel&version=1.0.0
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
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.
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.8)
- Optima.Net.Events (>= 1.0.3)
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