Domain model
Messages & slices
Commands, facts, public contracts, and state each have a distinct job.
Say what a message means
Messages.cs
C#
// An intention sent to exactly one owner
public sealed record ChargePayment(string OrderId, decimal Amount) : ICommand;
// An internal fact inside one bounded context
public sealed record PaymentCharged(string OrderId, decimal Amount) : IDomainEvent;
// A versioned public contract across boundaries
[IntegrationEvent("payments.payment-charged", Version = 1)]
public sealed record PaymentChargedV1(string OrderId, decimal Amount, string Currency)
: IIntegrationEvent;Model state as a closed set
Immutable union cases make impossible states unrepresentable, while Evolve remains an ordinary deterministic function.
Payment.cs
C#
[Union]
public abstract record Payment : IState<Payment>
{
public static Payment Initial { get; } = new None();
public Payment Evolve(IDomainEvent domainEvent) => (this, domainEvent) switch
{
(None, PaymentCharged charged) => new Charged(charged.Amount),
(Charged, PaymentRefunded) => new Refunded(),
_ => this,
};
public sealed record None : Payment;
public sealed record Charged(decimal Amount) : Payment;
public sealed record Refunded : Payment;
}Decide in one place
A command has exactly one owning slice. The analyzer catches ambiguous ownership at compile time.
ChargePaymentSlice.cs
C#
public sealed class ChargePaymentSlice : Slice<ChargePayment, Payment, PaymentCharged, PaymentError>
{
public override object KeyOf(ChargePayment command) => command.OrderId;
public override Result<PaymentCharged, PaymentError> Decide(ChargePayment command, Payment payment) => payment switch
{
Payment.None when command.Amount > 1_000m => PaymentError.CardDeclined,
Payment.None => new PaymentCharged(command.OrderId, command.Amount),
Payment.Charged or Payment.Refunded => PaymentError.AlreadyCharged,
};
} Documentation follows the repository source of truth.