Value Objects are highly customizable thanks to Roslyn Source Generators. This page covers all customization options available for Simple (`[ValueObject]`) and Complex (`[ComplexValueObject]`) Value Objects. Each section shows the relevant attribute properties and usage examples. * [Key Member Generation](#key-member-generation) * [Custom Equality Comparer](#custom-equality-comparer) * [Equality comparison of simple value objects](#equality-comparison-of-simple-value-objects) * [Equality comparison of complex value objects](#equality-comparison-of-complex-value-objects) * [Predefined and Custom Comparer-Accessors](#predefined-and-custom-comparer-accessors) * [Custom Comparer (simple value objects only)](#custom-comparer) * [Custom Type for Validation Errors](#custom-type-for-validation-errors) * [Constructor Access Modifier](#constructor-access-modifier) * [Factory Method Customization](#factory-method-customization) * [Renaming Factory Methods](#renaming-factory-methods) * [Disabling Factory Methods](#disabling-factory-methods) * [Setting Dependencies](#setting-dependencies) * [Null and Empty String Handling](#null-and-empty-string-handling) * [Null Value Handling](#null-value-handling) * [Empty String Handling](#empty-string-handling) * [Operator Customization](#operator-customization) * [Comparison Interfaces and Operators](#comparison-interfaces-and-operators) * [Skipping Equality Comparison](#skipping-equality-comparison) * [Arithmetic Operators](#arithmetic-operators) * [Parsing and Formatting](#parsing-and-formatting) * [IParsable<T> and ISpanParsable<T>](#iparsablet-and-ispanparsablet) * [Disabling parsing and formatting](#disabling-parsing-and-formatting) * [Default Struct Handling](#default-struct-handling) * [Type Conversion](#type-conversion) * [Key Member Conversion](#key-member-conversion) * [Custom Type Conversion](#custom-type-conversion) ## Key Member Generation The key member of a simple value object is generated by the source generator. Use `KeyMemberName`, `KeyMemberAccessModifier` and `KeyMemberKind` to change the generation of the key member, or set `SkipKeyMember` to `true` to provide custom implementation. Example: Let source generator generate property `public DateOnly Date { get; }` instead of field `private readonly DateOnly _value;` (Default). ```csharp [ValueObject(KeyMemberName = "Date", KeyMemberAccessModifier = AccessModifier.Public, KeyMemberKind = MemberKind.Property)] public readonly partial struct OpenEndDate { } ``` Example of custom implementation: ```csharp [ValueObject(SkipKeyMember = true, // We implement the key member "Date" ourselves KeyMemberName = nameof(Date))] // Source Generator needs to know the name we've chosen public readonly partial struct OpenEndDate { private readonly DateOnly? _date; private DateOnly Date { get => _date ?? DateOnly.MaxValue; init => _date = value; } } ``` ## Custom Equality Comparer By default, the source generator uses the default implementation of `Equals` and `GetHashCode` for all assignable properties and fields, except for `strings`. If the member is a `string`, then the source generator is using `StringComparer.OrdinalIgnoreCase`. Additionally, the analyzer will warn you if you don't provide an equality comparer for a string-based value object. ### Equality comparison of simple value objects Use `KeyMemberEqualityComparerAttribute` to define an equality comparer for comparison of key members and for computation of the hash code. Use one of the predefined `ComparerAccessors` or implement a new one. The example below changes the comparer from `OrdinalIgnoreCase` to `Ordinal`. ```csharp [ValueObject] [KeyMemberEqualityComparer] public sealed partial class ProductName { } ``` ### Equality comparison of complex value objects Use `MemberEqualityComparerAttribute` to change both, the equality comparer and the members being used for comparison and computation of the hash code. ```csharp [ComplexValueObject] public sealed partial class Boundary { // The equality comparison uses `Lower` only! [MemberEqualityComparer, decimal>] public decimal Lower { get; } public decimal Upper { get; } } ``` To use all *assignable* properties (properties with a getter that can be set via the constructor) in comparison, either don't use `MemberEqualityComparerAttribute` at all or put it on all members. ```csharp [ComplexValueObject] public sealed partial class Boundary { [MemberEqualityComparer, decimal>] public decimal Lower { get; } [MemberEqualityComparer, decimal>] public decimal Upper { get; } } ``` For complex value objects, you can also customize the string comparison behavior using the `DefaultStringComparison` property: ```csharp [ComplexValueObject(DefaultStringComparison = StringComparison.CurrentCulture)] public partial class MyValueObject { public string Property1 { get; } public string Property2 { get; } } ``` To exclude a property from generated equality, factory methods, and other generated code, use the `[IgnoreMember]` attribute: ```csharp [ComplexValueObject] public partial class MyValueObject { public string Name { get; } [IgnoreMember] public string DisplayLabel { get; } } ``` ### Predefined and Custom Comparer-Accessors Implement the interface `IEqualityComparerAccessor` to create a new custom accessor. The accessor has 1 property that returns an instance of `IEqualityComparer`. The generic type `T` is the type of the member to compare. ```csharp public interface IEqualityComparerAccessor { static abstract IEqualityComparer EqualityComparer { get; } } ``` Implementation of an accessor for members of type `string`. ```csharp public class StringOrdinal : IEqualityComparerAccessor { public static IEqualityComparer EqualityComparer => StringComparer.Ordinal; } ``` Predefined accessors in static class `ComparerAccessors`: ```csharp // Predefined: ComparerAccessors.StringOrdinal ComparerAccessors.StringOrdinalIgnoreCase ComparerAccessors.CurrentCulture ComparerAccessors.CurrentCultureIgnoreCase ComparerAccessors.InvariantCulture ComparerAccessors.InvariantCultureIgnoreCase ComparerAccessors.Default; // e.g. ComparerAccessors.Default or ComparerAccessors.Default ``` ## Custom Comparer > Note: This section covers `IComparable` and `IComparer` (for ordering). Do not confuse `IComparer` with `IEqualityComparer` (for equality and hash codes). A custom implementation of `IComparer` can be defined on simple value objects only. Use `KeyMemberComparerAttribute` to specify a comparer. Use one of the predefined `ComparerAccessors` or implement a new one (see below). ```csharp [ValueObject] [KeyMemberComparer] public sealed partial class ProductName { } ``` Implement the interface `IComparerAccessor` to create a new custom accessor. The accessor has 1 property that returns an instance of `IComparer`. The generic type `T` is the type of the member to compare. ```csharp public interface IComparerAccessor { static abstract IComparer Comparer { get; } } ``` Implementation of an accessor for members of type `string`. ```csharp public class StringOrdinal : IComparerAccessor { public static IComparer Comparer => StringComparer.Ordinal; } ``` Predefined accessors in static class `ComparerAccessors`: ```csharp // Predefined: ComparerAccessors.StringOrdinal ComparerAccessors.StringOrdinalIgnoreCase ComparerAccessors.CurrentCulture ComparerAccessors.CurrentCultureIgnoreCase ComparerAccessors.InvariantCulture ComparerAccessors.InvariantCultureIgnoreCase ComparerAccessors.Default; // e.g. ComparerAccessors.Default or ComparerAccessors.Default ``` ## Custom type for validation errors The default `ValidationError` class only carries a simple error message. For more complex validation scenarios, you can create a custom validation error type that carries additional information: 1. Create a class implementing `IValidationError` 2. Apply `ValidationErrorAttribute` to your value object 3. Use the custom error type in validation methods > Custom validation types must implement `ToString()` for proper framework integration (JSON serialization, error messages, etc.) The following example demonstrates all three steps: ```csharp // Custom validation error with additional information public class BoundaryValidationError : IValidationError { public string Message { get; } public decimal? Lower { get; } public decimal? Upper { get; } // Constructor for custom validation scenarios public BoundaryValidationError( string message, decimal? lower, decimal? upper) { Message = message; Lower = lower; Upper = upper; } // Required factory method for generated code public static BoundaryValidationError Create(string message) { return new BoundaryValidationError(message, null, null); } // Required for framework integration public override string ToString() { return $"{Message} (Lower={Lower}, Upper={Upper})"; } } // Using custom validation error [ComplexValueObject] [ValidationError] public sealed partial class Boundary { public decimal Lower { get; } public decimal Upper { get; } static partial void ValidateFactoryArguments( ref BoundaryValidationError? validationError, ref decimal lower, ref decimal upper) { if (lower > upper) { validationError = new BoundaryValidationError( "Lower boundary must be less than upper boundary", lower, upper); return; } // Normalize values lower = Math.Round(lower, 2); upper = Math.Round(upper, 2); } } ``` ## Constructor access modifier By default, value object constructors are `private`. You can change this using the `ConstructorAccessModifier` property: > Consider carefully before making constructors public. Factory methods provide better validation and framework integration. ```csharp // Simple value object with public constructor [ValueObject(ConstructorAccessModifier = AccessModifier.Public)] public sealed partial class ProductName { } // Complex value object with public constructor [ComplexValueObject(ConstructorAccessModifier = AccessModifier.Public)] public sealed partial class Boundary { public decimal Lower { get; } public decimal Upper { get; } } ``` ## Factory Method Customization The source generator creates factory methods for object creation and validation. You can customize these methods in several ways: ### Renaming Factory Methods You can change the default names of factory methods (`Create` and `TryCreate`): ```csharp // Simple value object with custom factory method names [ValueObject( CreateFactoryMethodName = "Parse", TryCreateFactoryMethodName = "TryParse")] public sealed partial class ProductName { } // Complex value object with custom factory method names [ComplexValueObject( CreateFactoryMethodName = "FromRange", TryCreateFactoryMethodName = "TryFromRange")] public sealed partial class Boundary { public decimal Lower { get; } public decimal Upper { get; } } ``` ### Disabling Factory Methods > **Warning:** Setting `SkipFactoryMethods = true` has wide-reaching implications beyond the factory methods themselves. The following features are **also** disabled: - **`TypeConverter` attribute** — not emitted on the type. - **`IObjectFactory` interface** — not implemented. - **Conversion operator from key type** — not generated (it relies on the factory method internally). - **`IParsable` and `ISpanParsable`** — forced to skip. - **Arithmetic operators** (addition, subtraction, multiplication, division) — forced to `None`. - **Serialization converters** (System.Text.Json, Newtonsoft.Json, MessagePack) — not generated. As a result, framework integration features like JSON serialization, model binding, and EF Core value conversion will **not work** out of the box. If you still need serialization support, you can add an `[ObjectFactory(UseForSerialization = ...)]` attribute to provide a custom factory that the serialization converters will use instead. You can disable factory method generation entirely: ```csharp // Simple value object without factory methods [ValueObject(SkipFactoryMethods = true)] public sealed partial class ProductName { } // Complex value object without factory methods [ComplexValueObject(SkipFactoryMethods = true)] public sealed partial class Boundary { public decimal Lower { get; } public decimal Upper { get; } } ``` ## Setting Dependencies Several attribute settings have cascading effects on other settings. Individual sections throughout this page also note these cascades where relevant. | Setting | Cascading Effect | |-------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `SkipFactoryMethods = true` | Skips `IParsable`, `ISpanParsable`; sets arithmetic operators to `None`; suppresses `TypeConverter`, `IObjectFactory`, key conversion operator, and serialization converters | | `SkipIParsable = true` | Skips `ISpanParsable` (inherits from `IParsable`) | | `SkipEqualityComparison = true` | Sets `ComparisonOperators` and `EqualityComparisonOperators` to `None` | | `EqualityComparisonOperators = None` | Sets `ComparisonOperators` to `None` (comparison requires equality) | | `ComparisonOperators` > `EqualityComparisonOperators` | `EqualityComparisonOperators` coerced upward to match | | `EmptyStringInFactoryMethodsYieldsNull = true` | Sets `NullInFactoryMethodsYieldsNull = true` | ## Null and Empty String Handling Factory methods provide special handling for null and empty string values: ### Null Value Handling By default, factory methods reject null values. You can change this behavior: ```csharp // Allow null values to return null [ValueObject( NullInFactoryMethodsYieldsNull = true)] public sealed partial class ProductName { } // Usage var name1 = ProductName.Create(null); // Returns null var name2 = ProductName.Create("Valid"); // Returns ProductName instance ``` ### Empty String Handling For string value objects, you can configure empty string handling: ```csharp // Treat empty/whitespace strings as null [ValueObject( EmptyStringInFactoryMethodsYieldsNull = true)] public sealed partial class ProductName { } // Usage var name1 = ProductName.Create(""); // Returns null var name2 = ProductName.Create(" "); // Returns null var name3 = ProductName.Create("Valid"); // Returns ProductName instance ``` ## Operator Customization Value objects support various operators and interfaces that can be customized or disabled: ### Comparison Interfaces and Operators Control implementation of comparison interfaces (`IComparable`, `IComparable`) and operators: ```csharp // Disable IComparable/IComparable implementation [ValueObject( SkipIComparable = true)] public readonly partial struct Amount { } // Configure comparison operators (>, >=, <, <=) [ValueObject( ComparisonOperators = OperatorsGeneration.DefaultWithKeyTypeOverloads)] public readonly partial struct Amount { } ``` > Setting `ComparisonOperators` affects `EqualityComparisonOperators` to ensure consistent behavior between comparison and equality operations. `EqualityComparisonOperators` can also be configured directly to control generation of equality operators (`==`, `!=`): ```csharp [ValueObject( EqualityComparisonOperators = OperatorsGeneration.DefaultWithKeyTypeOverloads)] public readonly partial struct Amount { } ``` ### Skipping Equality Comparison You can completely disable generation of equality comparison members using `SkipEqualityComparison`: ```csharp // For simple (keyed) value objects [ValueObject(SkipEqualityComparison = true)] public readonly partial struct Amount { } // For complex value objects [ComplexValueObject(SkipEqualityComparison = true)] public readonly partial struct DateRange { public DateOnly Start { get; } public DateOnly End { get; } } ``` When `SkipEqualityComparison` is set to `true`, the source generator **will not generate**: - `Equals` method overrides (both `Equals(object?)` and `Equals(T)`) - `GetHashCode` method override - Equality operators (`==` and `!=`) - `IEquatable` interface implementation - `IEqualityOperators` interface implementation > **Use with caution**: Setting `SkipEqualityComparison` to `true` also sets `ComparisonOperators` and `EqualityComparisonOperators` to `None`, effectively disabling all comparison and equality operators. **Use cases**: - When you need custom equality logic that differs from structural equality - When integrating with systems that require reference equality - When implementing custom equality comparers that cannot be expressed with the library's attributes **Important**: After setting `SkipEqualityComparison = true`, you are responsible for implementing custom equality members if needed. ### Arithmetic Operators Control implementation of arithmetic operators (`+`, `-`, `*`, `/`): ```csharp // Configure all arithmetic operators [ValueObject( // Enable key type overloads (e.g., Amount + decimal) AdditionOperators = OperatorsGeneration.DefaultWithKeyTypeOverloads, // + SubtractionOperators = OperatorsGeneration.DefaultWithKeyTypeOverloads, // - MultiplyOperators = OperatorsGeneration.DefaultWithKeyTypeOverloads, // * DivisionOperators = OperatorsGeneration.DefaultWithKeyTypeOverloads)] // / public readonly partial struct Amount { } // Disable specific operators [ValueObject( AdditionOperators = OperatorsGeneration.None, // No + operator MultiplyOperators = OperatorsGeneration.None)] // No * operator public readonly partial struct Amount { } ``` Available operator modes: - `None`: Operator not generated - `Default`: Standard operators between value objects - `DefaultWithKeyTypeOverloads`: Also generates operators with key member type (e.g., `Amount + decimal`) ## Parsing and Formatting Value objects implement several interfaces for string handling that can be customized: ### IParsable and ISpanParsable Keyed value objects automatically implement parsing interfaces based on their key type: **IParsable**: Automatically implemented when the key type implements `IParsable` or is `string`, providing `Parse` and `TryParse` methods for string-based parsing. **ISpanParsable**: Automatically implemented when the key type implements `ISpanParsable`, providing zero-allocation parsing using `ReadOnlySpan` for high-performance scenarios: ```csharp [ValueObject] public readonly partial struct CustomerId { } // ISpanParsable implementation // Zero-allocation parsing for high-performance scenarios ReadOnlySpan span = "12345".AsSpan(); bool success = CustomerId.TryParse(span, null, out CustomerId? id); // Works with numeric types (int, long, decimal, etc.) [ValueObject] public readonly partial struct Amount { } ReadOnlySpan amountSpan = "99.99".AsSpan(); Amount? amount = Amount.Parse(amountSpan, null); // Works with DateTime, Guid, and other ISpanParsable types [ValueObject] public readonly partial struct OrderDate { } [ValueObject] public readonly partial struct ProductId { } ``` **Supported key types for ISpanParsable**: All built-in .NET types that implement `ISpanParsable`, including: - Numeric types: `int`, `long`, `short`, `byte`, `sbyte`, `uint`, `ulong`, `ushort`, `float`, `double`, `decimal` - Date/Time types: `DateTime`, `DateTimeOffset`, `TimeSpan`, `DateOnly`, `TimeOnly` - Other types: `Guid`, `Version`, `IPAddress`, and more ### Disabling parsing and formatting ```csharp // Disable IParsable and ISpanParsable implementations (affects string parsing) [ValueObject(SkipIParsable = true)] public readonly partial struct Amount { } ``` > **Note**: Setting `SkipIParsable = true` implicitly disables both `IParsable` and `ISpanParsable` because `ISpanParsable` inherits from `IParsable`. ```csharp // Disable ISpanParsable only, while keeping IParsable [ValueObject(SkipISpanParsable = true)] public readonly partial struct Amount { } // Disable IFormattable implementation (affects custom formatting) [ValueObject(SkipIFormattable = true)] public readonly partial struct Amount { } // Disable ToString override (affects string representation) [ValueObject(SkipToString = true)] public readonly partial struct Amount { } // Can also be used with complex value objects [ComplexValueObject(SkipToString = true)] public sealed partial class Boundary { public decimal Lower { get; } public decimal Upper { get; } } ``` ## Default Struct Handling By default, struct value objects **disallow** `default(T)` and the parameterless constructor. The source generator implements `IDisallowDefaultValue`, and analyzer rule **TTRESG047** produces a compile-time error when a violation is detected: ```csharp [ValueObject] public partial struct Amount { } // TTRESG047: "The 'default' expression is not allowed for type 'Amount'" var a = default(Amount); // ❌ Error Amount b = new(); // ❌ Error ``` ### Opting In with AllowDefaultStructs Set `AllowDefaultStructs = true` to allow default values. When enabled, the source generator produces a static `Empty` property (initialized to `default`), similar to `Guid.Empty`: ```csharp [ValueObject(AllowDefaultStructs = true)] public partial struct Amount { } // Usage var zero = Amount.Empty; // static property, equivalent to default(Amount) Amount a = default; // No warning ``` Use `DefaultInstancePropertyName` to rename the generated property: ```csharp [ValueObject( AllowDefaultStructs = true, // Allow default value DefaultInstancePropertyName = "Zero")] // Changes the property name from "Empty" to "Zero" public partial struct Amount { } // Usage var zero = Amount.Zero; // Instead of Amount.Empty ``` The same options work for complex value object structs: ```csharp [ComplexValueObject( AllowDefaultStructs = true, // Allow default value DefaultInstancePropertyName = "Unbounded")] // Enables default(Boundary) public partial struct Boundary { public decimal Lower { get; } public decimal Upper { get; } } ``` ### Constraints `AllowDefaultStructs` must remain `false` (the default) when: - The key member type is a **reference type** — the default value would be `null`, which is invalid. Analyzer rule **TTRESG057** enforces this. - Any member itself **disallows default values** (i.e., another struct type implementing `IDisallowDefaultValue`). Analyzer rule **TTRESG058** enforces this. ## Type Conversion Value objects support various conversion options: ### Key Member Conversion Simple value objects can control how they convert to and from their key member type using three properties: * `ConversionToKeyMemberType`: Controls conversion from value object to key member type (default: `Implicit`) * `ConversionFromKeyMemberType`: Controls conversion from key member type to value object (default: `Explicit`) * `UnsafeConversionToKeyMemberType`: Controls conversion from reference type value object to value type key member (default: `Explicit`). For example, converting a `class ProductName` (reference type) to its `int` key (value type), which would return `default` if the instance is `null`. Each property can be set to: * `None`: No conversion operator is generated * `Implicit`: Generates an implicit conversion operator * `Explicit`: Generates an explicit conversion operator requiring a cast ```csharp [ValueObject( ConversionToKeyMemberType = ConversionOperatorsGeneration.Explicit, // To key type ConversionFromKeyMemberType = ConversionOperatorsGeneration.Implicit, // From key type UnsafeConversionToKeyMemberType = ConversionOperatorsGeneration.None)] // Reference to value type public partial struct Amount { } ``` > Note: `UnsafeConversionToKeyMemberType` only applies when converting from reference type value objects to value type key members. ### Custom Type Conversion With `ObjectFactoryAttribute`, you can implement additional methods to convert a Value Object from/to type `T`. This conversion can be one-way (`T` -> Value Object) or two-way (`T` <-> Value Object). Conversion from a `string` allows ASP.NET Model Binding to bind both Simple and Complex Value Objects. Applying `[ObjectFactory]` adds the interface `IObjectFactory`, requiring you to implement a `Validate` method: ```csharp [ComplexValueObject] [ObjectFactory] public partial class Boundary { public decimal Lower { get; } public decimal Upper { get; } public static ValidationError? Validate( string? value, IFormatProvider? provider, out Boundary? item) { item = null; if (value is null) return null; var parts = value.Split(":", StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); if (parts.Length != 2) return new ValidationError("Invalid format. Expected 'lower:upper', e.g. '1.5:2.5'"); if (!decimal.TryParse(parts[0], provider, out var lower) || !decimal.TryParse(parts[1], provider, out var upper)) return new ValidationError("Invalid numbers. Expected decimal values, e.g. '1.5:2.5'"); return Validate(lower, upper, out item); } } ``` The `ObjectFactoryAttribute` also supports framework integration flags to control how the factory is used across different frameworks: ```csharp [ComplexValueObject] [ObjectFactory( UseForSerialization = SerializationFrameworks.All, // JSON, MessagePack serialization UseForModelBinding = true, // ASP.NET Core model binding UseWithEntityFramework = true)] // EF Core value conversion public partial class Boundary { // ... } ``` Additionally, the attribute supports `HasCorrespondingConstructor` (for EF Core to bypass validation on load), multiple object factories on a single type, and zero-allocation JSON with `ReadOnlySpan` on .NET 9+. See the **[Object Factories](Object-Factories)** page for full details.