Skip to main content

Authoring reference

Supported underlying types

string, Guid, bool, char, every built-in integer (including Int128 and UInt128, which travel as JSON strings), decimal, double, float, DateOnly, TimeOnly, DateTime, DateTimeOffset, TimeSpan.

Declarative options on [ValueObject<T>]

OptionEffect
Pattern, MinLength, MaxLengthValidation, EF column size, OpenAPI schema.
Minimum, MaximumWritten in invariant culture, parsed at compile time.
ComparisonEquality, ordering and hashing for string value objects. Ordinal by default.
ValueSet = Closed + [KnownValue]Reference-data codes with a frozen lookup and a schema enum. Members of a closed set over a reference type are boxed once and shared, so the boxed paths allocate nothing.
ArithmeticOperators and generic math for numeric value objects. Every result is re-validated.
ImplicitConversionToValue, ExplicitConversionFromValueConversions, opt-in per type.
AllowEmpty, AllowDefaultLoosen the two defaults that exist to catch mistakes.

Hooks

A value object declares a rule by implementing an interface, so the compiler checks the signature: a mis-typed rule fails the build instead of being silently ignored. All are optional, and VO0011 reports a rule written without its interface — the one mistake the compiler cannot catch.

InterfaceMember
IValueObjectNormalizer<TValue>static TValue NormalizeValue(TValue value)
IValueObjectSpanNormalizerstatic string NormalizeValue(ReadOnlySpan<char> value) — string value objects only
IValueObjectValidator<TValue>static ValidationResult ValidateValue(in TValue value)
IValueObjectFormatter<TValue>static bool TryFormatValue(in TValue value, Span<char> destination, out int charsWritten, ReadOnlySpan<char> format, IFormatProvider? provider)
IValueObjectStringFormatter<TValue>static string FormatValue(in TValue value, ReadOnlySpan<char> format, IFormatProvider? provider)

NormalizeValue must be idempotent and must not reject: an unnormalizable value is rejected by ValidateValue. TryFormatValue, when present, takes over formatting entirely, including the default format.

Adding IValueObjectSpanNormalizer alongside IValueObjectNormalizer<string> lets parsing and JSON reading normalize straight from the text, so ingesting a value allocates the normalized string and nothing else. It halves what TryParse allocates, and makes deserializing a payload of value objects allocate exactly what deserializing the same payload of primitives does. Write the value-typed overload as a one-line delegation:

public readonly partial struct Iban : IValueObjectNormalizer<string>, IValueObjectSpanNormalizer
{
public static string NormalizeValue(string value) => NormalizeValue(value.AsSpan());

public static string NormalizeValue(ReadOnlySpan<char> value)
{
Span<char> buffer = value.Length <= 64 ? stackalloc char[64] : new char[value.Length];
// ... write the normalized characters into buffer ...
return new string(buffer[..length]);
}
}

The rules are public because a static interface member cannot be anything else. Normalize remains the member callers use: it guards against a null underlying value and then defers to NormalizeValue.

Diagnostics

IdSeverityMeaning
VO0001ErrorThe type is not partial.
VO0002ErrorThe type is not a readonly struct, or is a record.
VO0003ErrorUnsupported underlying type.
VO0004ErrorA bound could not be parsed.
VO0005ErrorA closed value set declares no value.
VO0006ErrorA known value has an unusable name.
VO0007ErrorArithmetic requested on a non-numeric type.
VO0008WarningLength constraints on a non-string type.
VO0009ErrorA containing type is not partial.
VO0010ErrorAn uninitialized value object.
VO0011WarningA rule written without declaring its hook interface, so the generator will never call it.
VO0013ErrorA known value could not be converted.
VO0014ErrorAn invalid regular expression.
VO0015ErrorA malformed entity identifier prefix.
VO0016ErrorTwo types claiming the same prefix.
VO0017ErrorA normalization hook on an entity identifier, which owns its own.
VO0018ErrorBoth [EntityId] and [ValueObject<T>] on one type.

Next: Design Decisions, for the reasoning behind the shape of this surface.