Model domain values with value objects
A string can be an email address, a product name, or a currency code. A Guid can identify any record in the system. Value objects replace those ambiguous primitives with types that carry one meaning and validate it once.
Trellis gives you two starting points:
Trellis.Coregenerates domain-specific scalar types such asCustomerName,OrderId, andQuantity.Trellis.Primitivesprovides common types such as EmailAddress, Money, and GeoCoordinate.
Choose the shape first
| Your value | Start with |
|---|---|
| A common email, URL, phone number, country code, amount, or percentage | Built-in value objects |
| One CLR value with a domain-specific name or rule | Create a custom scalar value |
| A finite set of named values with behavior | Model symbolic values with RequiredEnum |
| Several fields that form one value | ValueObject, or a structured built-in such as Money |
| A value that may be absent | Maybe<T>, wrapped around the value object |
This choice is about meaning, not storage. An OrderId may still occupy one uniqueidentifier column, but the compiler no longer lets you pass a CustomerId by mistake.
Run your first value-object pipeline
Create a console application and install Trellis.Primitives. The package brings in Trellis.Core and its source generator transitively.
dotnet new console -n ValueObjectDemo
cd ValueObjectDemo
dotnet add package Trellis.Primitives
Replace Program.cs with:
using Trellis;
using Trellis.Primitives;
var registration = Registration.TryCreate(
email: "ada@example.com",
name: " Ada Lovelace ");
if (!registration.TryGetValue(out var customer, out var error))
{
Console.Error.WriteLine(error);
return;
}
Console.WriteLine($"{customer.Name.Value} <{customer.Email.Value}>");
[Trim, NotDefault, StringLength(100, MinimumLength = 2)]
public sealed partial class CustomerName : RequiredString<CustomerName>;
public sealed record Registration(EmailAddress Email, CustomerName Name)
{
public static Result<Registration> TryCreate(string? email, string? name) =>
Result.Combine(
EmailAddress.TryCreate(email, nameof(email)),
CustomerName.TryCreate(name, nameof(name)))
.Map((validEmail, validName) => new Registration(validEmail, validName));
}
Run it:
dotnet run
The important work happens before Registration exists:
- EmailAddress.TryCreate validates the built-in value.
- The generated
CustomerName.TryCreatetrims and validates the custom value. - Result.Combine keeps both field failures instead of stopping at the first one.
Mapconstructs the record only after both values are valid.
The partial keyword is required. The source generator supplies Value, TryCreate, Create, parsing, equality, and JSON conversion for RequiredString
Put validation at the boundary
Use TryCreate for HTTP input, messages, files, and other untrusted data. It returns Result
Use Create only for trusted constants and test setup, where an invalid value is a programming error. For generated identifiers, RequiredGuidNewUniqueV7().
After the boundary, pass value-object-shaped commands and domain methods inward. That removes repeated string checks from handlers and makes invalid combinations harder to represent.
Continue by task
- Create custom scalar value objects for IDs, names, quantities, flags, and timestamps.
- Browse the built-in value objects before creating another email, URL, phone, or code type.
- Work with money and percentages when calculations must preserve validation failures.
- Measure distance and build geographic bounds.
- Model weekly local-time availability.
- Connect value objects to ASP.NET Core or Entity Framework Core.
For complete signatures, follow the generated .NET API links in each guide or browse the Trellis API catalog.