Purview.ValueObjects
1.1.0
dotnet add package Purview.ValueObjects --version 1.1.0
NuGet\Install-Package Purview.ValueObjects -Version 1.1.0
<PackageReference Include="Purview.ValueObjects" Version="1.1.0" />
<PackageVersion Include="Purview.ValueObjects" Version="1.1.0" />
<PackageReference Include="Purview.ValueObjects" />
paket add Purview.ValueObjects --version 1.1.0
#r "nuget: Purview.ValueObjects, 1.1.0"
#:package Purview.ValueObjects@1.1.0
#addin nuget:?package=Purview.ValueObjects&version=1.1.0
#tool nuget:?package=Purview.ValueObjects&version=1.1.0
Purview.ValueObjects
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 (or built-in string trim/casing) andOnValidatevalidationEmptyinstances, equality, comparison,CompareTo,ToString(including the underlying value'sIFormattableand format overloads), and implicit conversions- The underlying value's standard interfaces —
IEquatable<T>,ISpanFormattable,IUtf8SpanFormattable,IParsable<T>,ISpanParsable<T>, andIUtf8SpanParsable<T>— mirrored per type - 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 – reference
Microsoft.EntityFrameworkCoreand the generator emits mapping members (value converters, comparers, complex-type mapping) plus aConfigureValueObjectsextension for automatic mapping. Queries use the value object type directly — no.Valuerequired. - Domain models – the F#-style single-case union pattern in C#.
Install
dotnet add package Purview.ValueObjects
The package ships the runtime contracts ([Scalar], [ValueObject], IValueObject, ...), the source generator,
the diagnostic analyzer, and code fixes for the analyzer's diagnostics (for example VO1001 offers
Add 'partial' modifier). There is no dependency on any event-sourcing library.
Quick start
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"
For the common trim-and-canonicalize case, configure the normalization instead of writing the hook:
[Scalar<string>(Trim = true, Casing = StringCasing.LowerInvariant)]
public readonly partial record struct EmailAddress { }
A complex member uses [StringNormalize(Trim = true)], and [assembly: ValueObjectDefaults(Trim = true)]
applies to every string scalar and member. Hydrate is never normalized.
[Scalar] wraps a single primitive; [ValueObject] wraps multiple members. The manual [Scalar] form
declares the underlying property; the automatic [Scalar<T>] / [Scalar(typeof(T))] forms have the
generator declare it (and [Scalar<string>(Nullable = true)] expresses a nullable reference scalar).
Formatting and parsing
A scalar mirrors the formatting and parsing surface of the type it wraps. When the underlying value implements
IFormattable, ISpanFormattable, IUtf8SpanFormattable, IParsable<T>, ISpanParsable<T>, or
IUtf8SpanParsable<T>, the generated value object implements the same interfaces and forwards to the wrapped
value:
[Scalar<Guid>]
public readonly partial record struct InstallationId { }
var id = InstallationId.Create(Guid.NewGuid());
id.ToString("N", CultureInfo.InvariantCulture); // forwards to Guid.ToString("N", ...)
InstallationId.Parse("2f8a…", CultureInfo.InvariantCulture); // Create(...) — validates
InstallationId.TryParse("not-a-guid", null, out _); // false
Parse validates through Create; TryParse returns false (through TryCreate) for input that does not
parse or fails domain validation. Detection is per type and per target framework, so a scalar only gets the
interfaces its underlying type actually implements (string is parsable but not span-formattable; an enum is
span-formattable but not parsable). Numeric/arithmetic interfaces, IConvertible, and collection interfaces are
intentionally not mirrored.
JSON serialization
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. That generated converter is reflection-free and trim- and
Native AOT-safe; register ScalarJsonConverterFactory only for a hand-written scalar, in a host that is neither
trimmed nor AOT-compiled.
Entity Framework Core
When your project references Microsoft.EntityFrameworkCore, the generator emits an EF nested class per value
object (a ValueConverter/ValueComparer) and an assembly-level ConfigureValueObjects extension that maps
them automatically:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.ConfigureValueObjects(); // generated into your project
}
Scalar value objects convert to their underlying primitive column; complex value objects map as EF Core complex
types (EF Core 8+) by default or JSON columns via [ValueObject(EFMapping = EntityFrameworkMapping.Json)].
Queries compare the value object type directly — no .Value required — or the raw underlying value
(c.Email == "...", m.Id == guid). See the Entity Framework guide for automatic + manual mapping, keys and
indexes, generated key values, query filters, schema/migrations, assembly defaults, and the opt-out levels.
Validation with ZodSharp
Validate value objects with Purview.ZodSharp, a C# port of Zod:
- Generator-integrated – a
[Scalar]/[ValueObject]type that is also[ZodSchema]has its generatedCreatewired to the ZodSharp-generated schema (CreatethrowsZodExceptionon invalid input).ZodSchemaMode.InsteadOfHooksopts out of theOnValidatehook; implement the generatedOnZodValidate(RefineCtx<T>)hook to add Zod-compatible refinements. - Generated validators – annotate a type with
[ZodSchema]+ DataAnnotations to get a zero-allocation{Type}Schemavalidator. - Custom rules on scalars – Purview.ZodSharp ships a validation attribute for every built-in rule (for
example
[NonSentinel(Message = "…")]), and a custom[ZodRule]-mapped attribute validates the scalar as a unit and owns the reportedCode/Origin. A rule written for the underlying value is adapted automatically via the generator-emittedScalarRuleAdapter. - Schema-first – build a schema for the underlying value (
Z.String().Email(),Z.Number(),Z.Enum<>()) and construct the value object through its strictCreatefactory.
How it works
Create(...)is the strict creation path: normalize, validate, then construct.Hydrate(...)reconstructs from persisted data without re-validating and is the path used by EF provider conversions.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>.
Disabling the generator
Set DisableValueObjectsSourceGenerator to true in your project:
<PropertyGroup>
<DisableValueObjectsSourceGenerator>true</DisableValueObjectsSourceGenerator>
</PropertyGroup>
See the project documentation for the full guides: getting started, value object design, Entity Framework, and ZodSharp validation.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. net11.0 is compatible. |
-
net10.0
- No dependencies.
-
net11.0
- No dependencies.
-
net8.0
- No dependencies.
-
net9.0
- No dependencies.
NuGet packages (15)
Showing the top 5 NuGet packages that depend on Purview.ValueObjects:
| Package | Downloads |
|---|---|
|
Purview.EventSourcing
Core abstractions, aggregate types, facades, transactions, and dependency injection extensions for building event-sourced .NET applications. |
|
|
Purview.EventSourcing.AzureStorage
Azure Table Storage and Blob Storage event store provider for Purview EventSourcing, including snapshot and large-payload support. |
|
|
Purview.EventSourcing.SqlServer
Azure SQL and SQL Server event stream storage provider for Purview EventSourcing. |
|
|
Purview.EventSourcing.MongoDB
MongoDB event stream and snapshot storage provider for Purview EventSourcing. |
|
|
Purview.EventSourcing.CosmosDb
Azure Cosmos DB queryable snapshot storage provider for Purview EventSourcing. |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 1.1.0 | 0 | 10/16/2026 |
| 1.0.1 | 95 | 10/8/2026 |
| 1.0.0 | 100 | 10/7/2026 |
| 1.0.0-prerelease.11 | 73 | 9/30/2026 |
| 1.0.0-prerelease.10 | 71 | 9/29/2026 |
| 1.0.0-prerelease.9 | 145 | 9/28/2026 |
| 1.0.0-prerelease.8 | 67 | 9/28/2026 |
| 1.0.0-prerelease.7 | 79 | 9/27/2026 |
| 1.0.0-prerelease.6 | 159 | 9/25/2026 |
| 1.0.0-prerelease.5 | 144 | 9/24/2026 |
| 1.0.0-prerelease.4 | 73 | 9/20/2026 |
| 1.0.0-prerelease.3 | 81 | 9/20/2026 |
| 1.0.0-prerelease.2 | 158 | 9/18/2026 |
| 1.0.0-prerelease.1 | 77 | 9/18/2026 |