Skip to content

Latest commit

 

History

History
192 lines (144 loc) · 6.69 KB

File metadata and controls

192 lines (144 loc) · 6.69 KB

Getting Started

This guide walks through modeling DTOs and domain values with Purview.ValueObjects.

1. Reference the package

dotnet add package Purview.ValueObjects

The package includes the runtime contracts, the source generator, the diagnostic analyzer, and code fixes for the analyzer's diagnostics (for example VO1001 offers Add 'partial' modifier).

2. Scalar value objects

A scalar value object wraps a single primitive value. It is the F#-style single-case union for C#.

using Purview.ValueObjects.Serialization;

[Scalar]
public readonly partial record struct EmailAddress
{
    public string Value { get; }

    static partial void OnNormalize(ref string value) => value = value?.Trim().ToLowerInvariant()!;

    static partial void OnValidate(string value)
    {
        if (string.IsNullOrWhiteSpace(value))
            throw new ArgumentException("Email is required.", nameof(value));

        if (!value.Contains('@', StringComparison.Ordinal))
            throw new ArgumentException("Invalid email format.", nameof(value));
    }
}

The generator adds:

  • EmailAddress.Create(string) – normalizes, validates, then constructs. Throws on invalid input.
  • EmailAddress.Hydrate(string) – constructs without re-validating (for persisted data).
  • EmailAddress.TryCreate(string, out EmailAddress) – returns false instead of throwing.
  • EmailAddress.Empty – a default instance.
  • Equality, comparison, CompareTo, ToString, implicit conversions, and a JSON converter.
var email = EmailAddress.Create("  Demo@Example.COM  ");
// email.Value == "demo@example.com"

bool ok = EmailAddress.TryCreate("not-an-email", out _);
// ok == false

string json = System.Text.Json.JsonSerializer.Serialize(email);
// json == "\"demo@example.com\""

3. Complex value objects

A complex value object wraps multiple members and validates them together.

[ValueObject]
public readonly partial record struct Money
{
    public decimal Amount { get; }

    public CurrencyCode Currency { get; }

    partial void OnValidate(decimal amount, CurrencyCode currency)
    {
        if (amount < 0)
            throw new ArgumentOutOfRangeException(nameof(amount), "Amount cannot be negative.");

        if (currency == CurrencyCode.Empty)
            throw new ArgumentException("Currency cannot be empty.", nameof(currency));
    }
}

4. Contextual value objects

When validity depends on the owning instance, implement IContextualValueObject<TSelf, TValue, TOwner>:

[Scalar]
public readonly partial record struct OrderStatus
    : IContextualValueObject<OrderStatus, OrderStatusCode, Order>
{
    public OrderStatusCode Value { get; }

    public static OrderStatus Create(OrderStatusCode value, in ValueObjectContext<Order> context)
    {
        var current = context.Owner.Status.Value;
        return IsValidTransition(current, value)
            ? new(value)
            : throw new InvalidOperationException($"Invalid transition {current} -> {value}");
    }
}

ValueObjectContext<TOwner> carries the owner instance, the member name being assigned, and an optional reason.

5. JSON serialization

Scalar value objects serialize as their underlying value. The generator emits a [JsonConverter] per value object, so they round-trip with default options. For a shared options instance, register ScalarJsonConverterFactory:

var options = new JsonSerializerOptions();
options.Converters.Add(new ScalarJsonConverterFactory());

ValueObjectDeserializationMode controls which factory deserialization uses:

  • Hydrate (default) – reconstructs via Hydrate, skipping validation.
  • Strict – reconstructs via Create, re-running validation.
[Scalar(DeserializationMode = ValueObjectDeserializationMode.Strict)]
public readonly partial record struct EmailAddress
{
    // ...
}

6. Assembly-level defaults

Use [ValueObjectDefaults] to set generic defaults for the whole assembly. Every option that can be set on [Scalar]/[ValueObject] can be defaulted here (GenerateJsonConverter, GenerateComparable, GenerateComparisonOperators, GenerateEnumProperties, GenerateImplicitFromPrimitive, GenerateImplicitToPrimitive, GenerateEmpty, GenerateConstructor, GenerateEFConverter, GenerateEFComparer, EFMapping, DeserializationMode, and ZodSchemaMode):

[assembly: ValueObjectDefaults(
    GenerateConstructor = false,
    GenerateJsonConverter = false,
    ZodSchemaMode = ZodSchemaMode.InsteadOfHooks
)]

Assembly defaults apply to every value object in the assembly; an option explicitly set on an individual [Scalar]/[ValueObject] attribute always overrides it.

7. Validate with ZodSharp

Purview.ZodSharp is a C# port of Zod that can validate value objects. Add [ZodSchema] (plus DataAnnotations on the underlying value) to generate a zero-allocation validator, or build a schema for the raw value with Z.String(), Z.Number(), Z.Enum():

using System.ComponentModel.DataAnnotations;
using ZodSharp;

[Scalar]
[ZodSchema]
public readonly partial record struct EmailAddress
{
    [EmailAddress]
    [StringLength(254, MinimumLength = 3)]
    public string Value { get; }

    // ...
}

var email = EmailAddress.Create("demo@example.com");
var result = EmailAddressSchema.Validate(email);   // ValidationResult<EmailAddress>

Because EmailAddress is both [Scalar] and [ZodSchema], the generated Create also validates the constructed instance through EmailAddressSchema — EmailAddress.Create("not-an-email") throws a ZodException. Use ZodSchemaMode.InsteadOfHooks on the attribute to run the schema instead of the OnValidate hook.

In ASP.NET Core, Purview.ZodSharp.AspNetCore converts those ZodExceptions into standard Problem Details responses — combine ValueObjectDeserializationMode.Strict with AddZodSharpProblemDetails() + UseExceptionHandler() so invalid request bodies return HttpValidationProblemDetails automatically. See the src/src/ZodSharp.AspNetCoreSample project.

See ZodSharp-Validation.md and the src/src/ZodSharpSample project.

Next steps

  • Entity-Framework.md – mapping value objects to EF Core: automatic ConfigureValueObjects(), keys, generated key values, query filters, manual control, and schema/migration notes.
  • Value-Object-Design.md – where validation lives, the Create/Hydrate split, and how value objects sit in a domain model next to entities.
  • ZodSharp-Validation.md – validating value objects with Purview.ZodSharp.
  • The src/src/Sample, src/src/EFDomainSample.Persistence (domain + persistence split), and src/src/ZodSharpSample projects for runnable examples.