Source-generated scalar and complex value objects for .NET.
Adds F#-style single-case types to C#. Mark a partial struct or class with [Scalar] or [ValueObject] and the
incremental source generator produces:
Create/Hydrate/TryCreatefactories withOnNormalizenormalization andOnValidatevalidationEmptyinstances, equality, comparison,CompareTo,ToString, and implicit conversions- JSON converters (scalar value objects serialize as their underlying value)
- Contextual creation via
IContextualValueObject<,>+ValueObjectContext<T>
Use cases
- DTOs – strong, self-validating types with serialization/deserialization and business rules.
- Entity Framework – value objects map cleanly onto JSON columns via
ScalarJsonConverterFactory. - Domain models – the F#-style single-case union pattern in C#.
dotnet add package Purview.ValueObjects
The package ships the runtime contracts ([Scalar], [ValueObject], IValueObject, ...), the source generator,
and the diagnostic analyzer. There is no dependency on any event-sourcing library.
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));
}
}
var email = EmailAddress.Create("Demo@Example.com");
// email.Value == "demo@example.com"[Scalar] wraps a single primitive; [ValueObject] wraps multiple members.
Scalar value objects serialize as their underlying value. Register the converter factory on your
JsonSerializerOptions:
var options = new JsonSerializerOptions();
options.Converters.Add(new ScalarJsonConverterFactory());The generator also emits a [JsonConverter] per value object, so scalar/complex value objects serialize correctly
even when the factory is not registered.
Use the same options for Entity Framework JSON columns:
modelBuilder
.Entity<Customer>()
.Property(c => c.Email)
.HasColumnType("jsonb");See the src/src/Sample and src/src/ZodSharpSample projects for end-to-end examples and docs/ for guidance.
Validate value objects with Purview.ZodSharp, a C# port of Zod. Three patterns are supported:
- Generator-integrated – a value object annotated with both
[Scalar]/[ValueObject]and[ZodSchema]has its generatedCreatewired to the ZodSharp-generated schema (CreatethrowsZodExceptionon invalid input).ZodSchemaMode.InsteadOfHooksopts out of theOnValidatehook. - Generated validators – annotate a value object or DTO with
[ZodSchema]+ DataAnnotations; a source generator emits a zero-allocation{Type}Schemavalidator (EmailAddressSchema.Validate(email)). - Schema-first – build a schema for the scalar's underlying value (
Z.String().Email(),Z.Number(),Z.Enum<>()) and construct the value object through its strictCreatefactory.
using ZodSharp;
[Scalar]
[ZodSchema]
public readonly partial record struct EmailAddress
{
[EmailAddress]
public string Value { get; }
}
var result = EmailAddressSchema.Validate(EmailAddress.Create("demo@example.com"));See ZodSharp Validation, the src/src/ZodSharpSample project, and
the src/src/ZodSharp.AspNetCoreSample project (ASP.NET Core Problem Details for strict deserialization failures).
Create(...)is the strict creation path: normalize, validate, then construct.Hydrate(...)reconstructs from persisted data without re-validating.ValueObjectDeserializationModecontrols which factory JSON deserialization uses (Hydrateby default,Strictre-runs validation).- Contextual value objects (
IContextualValueObject<TSelf, TValue, TOwner>) validate against the owning instance throughValueObjectContext<TOwner>.
Set DisableValueObjectsSourceGenerator to true in your project:
<PropertyGroup>
<DisableValueObjectsSourceGenerator>true</DisableValueObjectsSourceGenerator>
</PropertyGroup>src/src/ValueObjects– runtime contracts and theScalarJsonConverterFactory.src/src/SourceGenerator– incremental source generator + analyzer.src/src/SourceGenerator.Refactorings– code fix for the "must be partial" diagnostic.src/tests– unit and source-generator tests.docs– design and usage guidance.