domain-modeling-and-primitiveslisted
Install: claude install-skill Sarmkadan/dotnet-senior-skills
# Domain Modeling and Primitives
## Primitive obsession: the threshold
A raw primitive is fine until it acquires rules or gets confused with its neighbors. The triggers for wrapping:
- Two same-typed parameters that must not be swapped: `Transfer(Guid fromAccountId, Guid toAccountId)` compiles happily with the arguments reversed. `AccountId` as a wrapped type turns the swap into a compile error.
- A validation rule enforced in more than one place: if `email` is regex-checked in the controller, the service, and the importer, the string should have been an `Email` type validating once, in its constructor.
- Money as `decimal`: adding EUR to USD compiles. `Money(decimal Amount, Currency Currency)` with an addition operator that throws on currency mismatch does not.
```csharp
// WRONG: every consumer re-validates or trusts blindly
public void Register(string email) { ... }
// RIGHT: an Email that exists is valid; the rule lives in one place
public readonly record struct Email
{
public string Value { get; }
public Email(string value)
{
if (!MailAddress.TryCreate(value, out _))
throw new ArgumentException($"Invalid email: '{value}'", nameof(value));
Value = value.Trim().ToLowerInvariant();
}
public override string ToString() => Value;
}
```
Do not wrap everything: a `PageNumber` type over an `int` used in one method is ceremony. The threshold is rules or confusability, not typing zeal. For EF mapping, value objects bind via `HasCo