* [Requirements](#requirements) * [Getting started](#getting-started) * [Simple value objects](#simple-value-objects) * [Complex value objects](#complex-value-objects) * [Validation](#validation) * [Validation of the factory method arguments](#validation-of-the-factory-method-arguments) * [Validation of the constructor arguments](#validation-of-the-constructor-arguments) * [Customizing](#customizing) * [Key member generation](#key-member-generation) * [Custom equality comparer](#custom-equality-comparer) * [Custom comparer (simple value objects only)](#custom-comparer) * [Custom type for validation errors](#custom-type-for-validation-errors) * [Constructor access modifier](#constructor-access-modifier) * [Rename factory methods](#rename-factory-methods) * [Skip factory methods generation](#skip-factory-methods-generation) * [Null in factory methods yields null](#null-in-factory-methods-yields-null) * [Empty-String in factory methods yields null](#empty-string-in-factory-methods-yields-null) * [Skip implementation of `IComparable`/`IComparable`](#skip-implementation-of-icomparableicomparablet) * [Implementation of addition operators](#implementation-of-addition-operators) * [Implementation of subtraction operators](#implementation-of-subtraction-operators) * [Implementation of multiply operators](#implementation-of-multiply-operators) * [Implementation of division operators](#implementation-of-division-operators) * [Implementation of comparison operators](#implementation-of-comparison-operators) * [Skip implementation of `IParsable`](#skip-implementation-of-iparsablet) * [Skip implementation of `IFormattable`](#skip-implementation-of-iformattable) * [Changing the name of static property `Empty`](#changing-the-name-of-static-property-empty) * [Convert from/to any specific type](#convert-fromto-any-specific-type) * [JSON serialization](#json-serialization) * [Option 1: Make project with Value Objects depend on corresponding Nuget package](#option-1-make-project-with-value-objects-depend-on-corresponding-nuget-package) * [Option 2: Register JSON converter with JSON serializer settings](#option-2-register-json-converter-with-json-serializer-settings) * [MessagePack serialization](#messagepack-serialization) * [Option 1: Make project with Value Objects depend on Nuget package](#option-1-make-project-with-value-objects-depend-on-nuget-package) * [Option 2: Register MessagePack FormatterResolver with MessagePack serializer options](#option-2-register-messagepack-formatterresolver-with-messagepack-serializer-options) * [Support for Minimal Api Parameter Binding and ASP.NET Core Model Binding](#support-for-minimal-api-parameter-binding-and-aspnet-core-model-binding) * [Minimal Api](#minimal-api) * [ASP.NET Core MVC (Controllers)](#aspnet-core-mvc-controllers) * [Support for Entity Framework Core](#support-for-entity-framework-core) * [Option 1: Manual registration of the ValueConverter](#option-1-manual-registration-of-the-valueconverter) * [Option 2: Registration of the ValueConverter via extension method for `ModelBuilder`](#option-2-registration-of-the-valueconverter-via-extension-method-for-modelbuilder) * [Option 3: Registration of the ValueConverter via extension method for `DbContextOptionsBuilder`](#option-3-registration-of-the-valueconverter-via-extension-method-for-dbcontextoptionsbuilder) * [Logging](#logging-v610-or-higher) * [Real-world use cases and ideas](#real-world-use-cases-and-ideas) * [Open-ended End Date](#open-ended-end-date) * [(Always-positive) Amount](#always-positive-amount) This library provides an easy way for implementation of simple and complex **Value Objects**. The library comes with some Roslyn Analyzers and Code Fixes to guide the software developers through the implementation. Furthermore, additional Nuget packages add support for `System.Text.Json`, `Newtonsoft.Json`, `MessagePack`, `Entity Framework Core` and `ASP.NET Core Model Binding`. ## Requirements * C# 11 (or higher) for generated code * SDK 8.0.400 (or higher) for building projects ## Getting started Required Nuget package: [![Thinktecture.Runtime.Extensions](https://img.shields.io/nuget/v/Thinktecture.Runtime.Extensions.svg?maxAge=60&label=Thinktecture.Runtime.Extensions)](https://www.nuget.org/packages/Thinktecture.Runtime.Extensions/) The value objects described here are divided in 2 categories: * Simple or *keyed* value objects are types with 1 field/property (aka *key member*), which share a lot of features with [Smart Enums](Smart-Enums) * Complex value objects are types with 2 or more fields/properties ### Simple value objects A simple value object has 1 field/property only, i.e., it is kind of *wrapper* for another (primitive) type. The main use case is to prevent creation of values/instances which are considered invalid according to some *rules*. In DDD (domain-driven design), working with primitive types, like `string`, directly is called **primitive obsession** and should be avoided. A value object can be an immutable `class` or a `readonly struct` flagged with `ValueObjectAttribute`. > The property or field of type T of a simple value object will be called the **key member** from now on. Most simple value objects with a key member of type `string` and another one (which is a `struct`) with an `int`. ```C# [ValueObject] public sealed partial class ProductName { } [ValueObject] public readonly partial struct Amount { } ``` After the implementation of the `ProductName`, a Roslyn source generator kicks in and implements the rest. Following API is available from now on. ```C# // Factory method for creation of new instances. // Throws ValidationException if the validation fails (if we had any) ProductName apple = ProductName.Create("Apple"); // Alternatively, using an explicit cast, which behaves the same way as calling "ProductName.Create" ProductName apple = (ProductName)"Apple"; ----------- // The same as above but returns a bool instead of throwing an exception (dictionary-style) bool created = ProductName.TryCreate("Chocolate", out ProductName? chocolate); ----------- // Similar to TryCreate but returns a ValidationError instead of a boolean. ValidationError? validationError = ProductName.Validate("Chocolate", null, out var chocolate); if (validationError is null) { logger.Information("Product name {Name} created", chocolate); } else { logger.Warning("Failed to create product name. Validation result: {validationError}", validationError.ToString()); } ----------- // Implicit conversion to the type of the key member string valueOfTheProductName = apple; // "Apple" ----------- // Equality comparison compares the key member using default comparer by default. // Key members of type `string` are compared with 'StringComparer.OrdinalIgnoreCase' by default. bool equal = apple.Equals(apple); ----------- // Equality comparison operators: '==' and '!=' bool equal = apple == apple; bool notEqual = apple != apple; ----------- // Hash code: combined hash code of type and key member. // Strings are using 'StringComparer.OrdinalIgnoreCase' by default. int hashCode = apple.GetHashCode(); ----------- // 'ToString' implementation return the string representation of the key member string value = apple.ToString(); // "Apple" ------------ // Implements IParsable which is especially helpful with minimal apis. bool success = ProductName.TryParse("New product name", null, out var productName); ProductName productName = ProductName.Parse("New product name", null); ------------ // Implements "IFormattable" if the key member is an "IFormattable". Amount amount = Amount.Create(42); string formattedValue = amount.ToString("000", CultureInfo.InvariantCulture); // "042" ------------ // Implements "IComparable" if the key member is an "IComparable", // or if custom comparer is provided. Amount amount = Amount.Create(1); Amount otherAmount = Amount.Create(2); int comparison = amount.CompareTo(otherAmount); // -1 ------------ // Implements comparison operators (<,<=,>,>=) if the key member has comparison operators itself. bool isBigger = amount > otherAmount; // Implements comparison operators to compare the value object with an instance of key-member-type, // if "ComparisonOperators" is set "OperatorsGeneration.DefaultWithKeyTypeOverloads" bool isBigger = amount > 2; ------------ // Implements addition / subtraction / multiplication / division if the key member supports corresponding operators Amount sum = amount + otherAmount; // Implements operators that accept an instance of key-member-type, // if the "OperatorsGeneration" is set "DefaultWithKeyTypeOverloads" Amount sum = amount + 2; ------------ // Provides a static default value "Empty" (similar to "Guid.Empty"), if the value object is a struct Amount defaultValue = Amount.Empty; // same as "Amount defaultValue = default;" ``` Additionally, the source generator implements a `TypeConverter` which is used by some libraries/frameworks like JSON serialization or ASP.NET Core Model Binding. ```C# TypeConverter typeConverter = TypeDescriptor.GetConverter(typeof(ProductName)); string value = (string)typeConverter.ConvertTo(apple, typeof(string)); // "Apple" ProductName productName = (ProductName)typeConverter.ConvertFrom("Apple"); ``` ### Complex value objects A complex value object is an immutable `class` or a `readonly struct` with a `ComplexValueObjectAttribute`. Complex value object usually has multiple readonly fields/properties. > Although a complex value object can have 1 field/property (or no members at all), it won't get the same treatment as a simple value object. A simple example would be a `Boundary` with 2 properties. One property is the lower boundary and the other is the upper boundary. Yet again, we skip the validation at the moment. ```C# [ComplexValueObject] public sealed partial class Boundary { public decimal Lower { get; } public decimal Upper { get; } } ``` The rest is implemented by a Roslyn source generator, providing the following API: ```C# // Factory method for creation of new instances. // Throws ValidationException if the validation fails (if we had any) Boundary boundary = Boundary.Create(lower: 1, upper: 2); ----------- // the same as above but returns a bool instead of throwing an exception (dictionary-style) bool created = Boundary.TryCreate(lower: 1, upper: 2, out Boundary? boundary); ----------- // similar to TryCreate but returns a ValidationError instead of a boolean. ValidationError? validationError = Boundary.Validate(lower: 1, upper: 2, out Boundary? boundary); if (validationError is null) { logger.Information("Boundary {Boundary} created", boundary); } else { logger.Warning("Failed to create boundary. Validation result: {validationError}", validationError.ToString()); } ----------- // Equality comparison compares the members using default or custom comparers. // Strings are compared with 'StringComparer.OrdinalIgnoreCase' by default. bool equal = boundary.Equals(boundary); ----------- // Equality comparison with '==' and '!=' bool equal = boundary == boundary; bool notEqual = boundary != boundary; ----------- // Hash code of the members according default or custom comparers int hashCode = boundary.GetHashCode(); ----------- // 'ToString' implementation string value = boundary.ToString(); // "{ Lower = 1, Upper = 2 }" ``` ## Validation Until now, the value objects were more or less simple classes without any validation. ### Validation of the factory method arguments Both, the simple and complex value objects have a `partial` method `ValidateFactoryArguments` to implement custom validation in. The implementation of `ValidateFactoryArguments` should not throw exceptions but use the `ValidationError`. > The parameters are passed-in "by-ref" in order to adjust them (like removing leading/trailing whitespaces). ```C# [ValueObject] public sealed partial class ProductName { static partial void ValidateFactoryArguments(ref ValidationError? validationError, ref string value) { if (String.IsNullOrWhiteSpace(value)) { value = null!; // Disable NRT compiler warning because we return an error anyway validationError = new ValidationError("Product name cannot be empty."); return; } if (value.Length == 1) { validationError = new ValidationError("Product name cannot be 1 character long."); return; } value = value.Trim(); } } ``` The implementation of `ValidateFactoryArguments` of a complex value object looks very similar. ```C# [ComplexValueObject] public sealed partial class Boundary { public decimal Lower { get; } public decimal Upper { get; } static partial void ValidateFactoryArguments(ref ValidationError? validationError, ref decimal lower, ref decimal upper) { if (lower <= upper) return; validationError = new ValidationError($"Lower boundary '{lower}' must be less than upper boundary '{upper}'"); } } ``` ### Validation of the constructor arguments In addition to the `partial` method `ValidateFactoryArguments` for [validation of factory method arguments](#validation-of-the-factory-method-arguments) there is another `partial` method `ValidateConstructorArguments`. The method `ValidateConstructorArguments` is being called in the `private` constructor implemented by the Roslyn source generator. > I highly recommend to prefer `ValidateFactoryArguments` over `ValidateConstructorArguments` because a constructor has no other options as to throw an exception, which will result in worse integration with the libraries and frameworks, like JSON serialization, ASP.NET Core model binding/validation and Entity Framework Core. ```C# [ValueObject] public sealed partial class ProductName { static partial void ValidateConstructorArguments(ref string value) { // do something } } ``` And the `ValidateConstructorArguments` of a complex value object `Boundary`. ```C# [ComplexValueObject] public sealed partial class Boundary { public decimal Lower { get; } public decimal Upper { get; } static partial void ValidateConstructorArguments(ref decimal lower, ref decimal upper) { // do something } } ``` ## Customizing ### 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 = ValueObjectAccessModifier.Public, KeyMemberKind = ValueObjectMemberKind.Property)] public readonly partial struct EndDate { } ``` Example of custom implementation: ```csharp [ValueObject(SkipKeyMember = true, // We implement the key member "Date" ourselves KeyMemberName = "Date")] // Source Generator needs to know the name we've chosen public readonly partial struct EndDate { private readonly DateOnly? _date; private DateOnly Date { get => _date ?? DateOnly.MaxValue; init => _date = value; } } ``` ### Custom equality comparer By default, the source generator is using 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`. > The reason strings are not using default implementation is, because I encountered very few use cases where the comparison must be performed case-sensitive. Case-sensitive string comparisons, encountered in the past, were almost all bugs because the developers have forgotten to pass appropriate (case-insensitive) comparer. #### Equality comparison of simple value objects Use `ValueObjectKeyMemberEqualityComparerAttribute` 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 (see few sections below). The example below changes the comparer from `OrdinalIngoreCase` to `Ordinal`. ```C# [ValueObject] [ValueObjectKeyMemberEqualityComparer] public sealed partial class ProductName { } ``` #### Equality comparison of complex value objects Use `ValueObjectMemberEqualityComparerAttribute` to change both, the equality comparer and the members being used for comparison and computation of the hash code. ```C# [ComplexValueObject] public sealed partial class Boundary { // The equality comparison uses `Lower` only! [ValueObjectMemberEqualityComparer, decimal>] public decimal Lower { get; } public decimal Upper { get; } } ``` To use all *assignable* properties in comparison, either don't use `ValueObjectMemberEqualityComparerAttribute` at all or put it on all members. ```C# [ComplexValueObject] public sealed partial class Boundary { [ValueObjectMemberEqualityComparer, decimal>] public decimal Lower { get; } [ValueObjectMemberEqualityComparer, decimal>] public decimal Upper { 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. ```C# 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`: ```C# // Predefined: ComparerAccessors.StringOrdinal ComparerAccessors.StringOrdinalIgnoreCase ComparerAccessors.CurrentCulture ComparerAccessors.CurrentCultureIgnoreCase ComparerAccessors.InvariantCulture ComparerAccessors.InvariantCultureIgnoreCase ComparerAccessors.Default; // e.g. ComparerAccessors.Default or ComparerAccessors.Default ``` ### Custom comparer A custom implementation of `IComparer` can be defined on simple value objects only. > Please note that this section is about implementation of `IComparable` and `IComparer`. Don't confuse the `IComparer` with `IEqualityComparer` which is being used for equality comparison and the computation of the hash code. Use `ValueObjectKeyMemberComparerAttribute` to specify a comparer. Use one of the predefined `ComparerAccessors` of implement a new one (see below). ```C# [ValueObject] [ValueObjectKeyMemberComparer] 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.OrdinalIgnoreCase; } ``` Predefined accessors in static class `ComparerAccessors`: ```C# // 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 class `ValidationError` is being used to pass the validation error from the method `Validate` to its caller. The `ValidationError` accepts nothing else besides an error message. If you want to pass more information to the caller then use `ValueObjectValidationErrorAttribute` to define your own class to be used for carrying validation error(s). In order to use the custom type with `ValueObjectValidationErrorAttribute`, you must create a class that implements the interface `IValidationError`. This interface requires an implementation of the factory method `static T Create(string message)`. The factory method will be used in generated code. Add additional constructors or factory methods for usage in your code as you desire. > Important! Custom type must implement `ToString()` because it is being used by some components, like JSON-(de)serializer, to get the actual error message. Here is an example of a custom validation error that accepts an error message and the parameters that were validated. ```csharp public class BoundaryValidationError : IValidationError { public string Message { get; } public decimal? Lower { get; } public decimal? Upper { get; } // for use in custom code public BoundaryValidationError( string message, decimal? lower, decimal? upper) { Message = message; Lower = lower; Upper = upper; } // mainly for generated code public static BoundaryValidationError Create(string message) { return new BoundaryValidationError(message, null, null); } // generated code will use ToString() to get the error message public override string ToString() { return $"{Message} (Lower={Lower}|Upper={Upper})"; } } ``` Apply the attribute to Value Object and start using the `BoundaryValidationError` in your validations. ```csharp [ComplexValueObject] [ValueObjectValidationError] 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) return; validationError = new BoundaryValidationError("Lower boundary must be less than upper boundary.", lower, upper); } } ``` ### Constructor access modifier The constructors of value objects are `private` by default. Use `ConstructorAccessModifier` to change the access modifier. ```csharp [ValueObject(ConstructorAccessModifier = ValueObjectAccessModifier.Public)] public sealed partial class ProductName { } ----------------------- [ComplexValueObject(ConstructorAccessModifier = ValueObjectAccessModifier.Public)] public sealed partial class Boundary { } ``` ### Rename factory methods It is possible to change the name of the factory methods `Create` and `TryCreate`. ```C# [ValueObject(CreateFactoryMethodName = "Create", TryCreateFactoryMethodName = "TryCreate")] public sealed partial class ProductName { } ------------ [ComplexValueObject(CreateFactoryMethodName = "Create", TryCreateFactoryMethodName = "TryCreate")] public sealed partial class BoundaryWithCustomFactoryNames { public decimal Lower { get; } public decimal Upper { get; } } ``` ### Skip factory methods generation It is possible to skip the generation of the factory methods `Create`/`TryCreate`/`Validate` but this comes with a price. Some features like JSON (de)serialization or ASP.NET Core model binding depend on the factory methods. If there are no factory methods then neither JSON converter nor ASP.NET Core model binder are going to be implemented. ```C# [ValueObject(SkipFactoryMethods = true)] public sealed partial class ProductName { } ------------ [ComplexValueObject(SkipFactoryMethods = true)] public sealed partial class BoundaryWithCustomFactoryNames { public decimal Lower { get; } public decimal Upper { get; } } ``` ### Null in factory methods yields null By default, providing `null` to methods `Create` and `TryCreate` of a simple value object is not allowed. If property `NullInFactoryMethodsYieldsNull` is set to `true`, then providing a `null` will return `null`. ```C# [ValueObject(NullInFactoryMethodsYieldsNull = true)] public sealed partial class ProductName { } ``` ### Empty-String in factory methods yields null Similar as with [NullInFactoryMethodsYieldsNull](#null-in-factory-methods-yields-null) described above, but for empty strings. If this property is set to `true` then the factory methods `Create` and `TryCreate` will return `null` if they are provided `null`, an empty string or a string containing white spaces only. ```C# [ValueObject(EmptyStringInFactoryMethodsYieldsNull = true)] public sealed partial class ProductName { public string Value { get; } } ``` ### Skip implementation of `IComparable`/`IComparable` Use `ValueObjectAttribute` to set `SkipIComparable` to `true` to disable the implementation of `IComparable` and `IComparable`. ```c# [ValueObject(SkipIComparable = true)] public readonly partial struct Amount { } ``` ### Implementation of addition operators Use `ValueObjectAttribute` to set `AdditionOperators` to `OperatorsGeneration.None` to disable the implementation of addition operators: `+`. Set the property to `OperatorsGeneration.DefaultWithKeyTypeOverloads` to generate additional operators to be able to perform addition of a Value Object with a value of the key-member-type. ```c# [ValueObject(AdditionOperators = OperatorsGeneration.None)] public readonly partial struct Amount { } ``` ### Implementation of subtraction operators Use `ValueObjectAttribute` to set `SubtractionOperators` to `OperatorsGeneration.None` to disable the implementation of addition operators: `-`. Set the property to `OperatorsGeneration.DefaultWithKeyTypeOverloads` to generate additional operators to be able to perform subtraction of a Value Object with a value of the key-member-type. ```c# [ValueObject(SubtractionOperators = OperatorsGeneration.None)] public readonly partial struct Amount { } ``` ### Implementation of multiply operators Use `ValueObjectAttribute` to set `MultiplyOperators` to `OperatorsGeneration.None` to disable the implementation of addition operators: `*`. Set the property to `OperatorsGeneration.DefaultWithKeyTypeOverloads` to generate additional operators to be able to perform multiplication of a Value Object with a value of the key-member-type. ```c# [ValueObject(MultiplyOperators = OperatorsGeneration.None)] public readonly partial struct Amount { } ``` ### Implementation of division operators Use `ValueObjectAttribute` to set `DivisionOperators` to `OperatorsGeneration.None` to disable the implementation of addition operators: `/`. Set the property to `OperatorsGeneration.DefaultWithKeyTypeOverloads` to generate additional operators to be able to perform division of a Value Object with a value of the key-member-type. ```c# [ValueObject(DivisionOperators = OperatorsGeneration.None)] public readonly partial struct Amount { } ``` ### Implementation of comparison operators Use `ValueObjectAttribute` to set `ComparisonOperators` to `OperatorsGeneration.None` to disable the implementation of comparison operators: `>`, `>=`, `<`, `<=`. Set the property to `OperatorsGeneration.DefaultWithKeyTypeOverloads` to generate additional operators to be able to compare a Value Object with a value of the key-member-type. ```c# [ValueObject(ComparisonOperators = OperatorsGeneration.None)] public readonly partial struct Amount { } ``` ### Skip implementation of `IParsable` Use `ValueObjectAttribute` to set `SkipIParsable` to `true` to disable the implementation of `IParsable`. ```c# [ValueObject(SkipIParsable = true)] public readonly partial struct Amount { } ``` ### Skip implementation of `IFormattable` Use `ValueObjectAttribute` to set `SkipIFormattable` to `true` to disable the implementation of `IFormattable`. ```c# [ValueObject(SkipIFormattable = true)] public readonly partial struct Amount { } ``` ### Skip implementation of `ToString` Use `ValueObjectAttribute` or `ComplexValueObjectAttribute` to set `SkipToString` to `true` to disable the implementation of the method `ToString()`. ```c# [ValueObject(SkipToString = true)] public readonly partial struct Amount { } --------- [ComplexValueObject(SkipToString = true)] public sealed partial class Boundary { public decimal Lower { get; } public decimal Upper { get; } } ``` ### Changing the name of static property `Empty` Use `ValueObjectAttribute` or `ComplexValueObjectAttribute` to change the name via `DefaultInstancePropertyName`. > For `structs` only. ```C# [ValueObject(DefaultInstancePropertyName = "Zero")] public readonly partial struct Amount { } // Usage var zero = Amount.Zero; // instead of Amount.Empty ------------- [ComplexValueObject(DefaultInstancePropertyName = "Unbounded")] public readonly partial struct BoundaryWithCustomFactoryNames { public decimal Lower { get; } public decimal Upper { get; } } ``` ### Convert from/to any specific type With `ValueObjectFactory` you can implement additional methods to be able 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. Example: Given is a complex Value Object `Boundary` having 2 decimals. The Value Object `Boundary` must be convertible from specific strings, e.g. the value `1:2` should return an instance with `Lower=1` and `Upper=2`. By applying `ValueObjectFactoryAttribute` the source generator adds the interface `IValueObjectFactory` which forces you to implement new method `Validate(string, IFormatProvider, out Boundary)`. With the new method `Validate` we have a one-way conversion, i.e. from `string` to `Boundary`. ```csharp [ComplexValueObject] [ValueObjectFactory] public sealed partial class Boundary { public decimal Lower { get; } public decimal Upper { get; } public static ValidationError? Validate(string? value, IFormatProvider? provider, out Boundary? item) { // splits strings like "1:2" and creates new instance of "Boundary" 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."); if (!Decimal.TryParse(parts[0], provider, out var lower) || !Decimal.TryParse(parts[1], provider, out var upper)) return new ValidationError("The provided values are not numbers."); // Delegate the call to "regular" implementation for further validation. // Alternatively, create a new instance here if there are no further validation. return Validate(lower, upper, out item); } } ``` Two-way conversion is required if the type `T` must be used for serialization/deserialization. This can be achieved by using the property `UseForSerialization = SerializationFrameworks.All`. This leads to implementation of the interface `IValueObjectConvertable` which consists of the method `T ToValue()`. The serialization frameworks (like `System.Text.Json`) specified by `SerializationFrameworks` are starting to prefer the newly implemented methods instead of using the regular serialization, i.e. serializing the complex value object as an object with 2 properties. ```csharp [ComplexValueObject] [ValueObjectFactory(UseForSerialization = SerializationFrameworks.All)] public sealed partial class Boundary { public decimal Lower { get; } public decimal Upper { get; } public static ValidationError? Validate(string? value, IFormatProvider? provider, out Boundary? item) { ... } public string ToValue() { return $"{Lower}:{Upper}"; } } ``` ## JSON serialization Depending on the concrete JSON library you use, you need a different Nuget package: * For `System.Text.Json`: [![Thinktecture.Runtime.Extensions.Json](https://img.shields.io/nuget/v/Thinktecture.Runtime.Extensions.Json.svg?maxAge=60&label=Thinktecture.Runtime.Extensions.Json)](https://www.nuget.org/packages/Thinktecture.Runtime.Extensions.Json/) * For `Newtonsoft.Json`: [![Thinktecture.Runtime.Extensions.Newtonsoft.Json](https://img.shields.io/nuget/v/Thinktecture.Runtime.Extensions.Newtonsoft.Json.svg?maxAge=60&label=Thinktecture.Runtime.Extensions.Newtonsoft.Json)](https://www.nuget.org/packages/Thinktecture.Runtime.Extensions.Newtonsoft.Json/) There are 2 options to make the Value Objects *JSON convertible*. ### Option 1: Make project with Value Objects depend on corresponding Nuget package The easiest way is to make [Thinktecture.Runtime.Extensions.Json](https://www.nuget.org/packages/Thinktecture.Runtime.Extensions.Json) / [Thinktecture.Runtime.Extensions.Newtonsoft.Json](https://www.nuget.org/packages/Thinktecture.Runtime.Extensions.Newtonsoft.Json) a dependency of the project(s) the value objects are in. The dependency doesn't have to be a direct one but can be transitive as well. Both Nuget packages lead to generation of JSON converters and flag the value object with a `JsonConverterAttribute`. This way the value objects can be converted to and from JSON without extra code. ### Option 2: Register JSON converter with JSON serializer settings > For simple value objects only. If making previously mentioned Nuget package a dependency of project(s) with value objects is not possible or desirable, then the other option is to register a JSON converter with *JSON serializer settings*. By using a JSON converter directly, the Nuget package can be installed in any project where the *JSON settings* are configured. * Use `ValueObjectJsonConverterFactory` with `System.Text.Json` * Use `ValueObjectNewtonsoftJsonConverter` with `Newtonsoft.Json` An example for ASP.NET Core application using `System.Text.Json`: ```C# var webHost = new HostBuilder() .ConfigureServices(collection => { collection.AddMvc() .AddJsonOptions(options => options.JsonSerializerOptions .Converters .Add(new ValueObjectJsonConverterFactory())); }) ``` An example for minimal apis: ```C# var builder = WebApplication.CreateBuilder(); builder.Services .ConfigureHttpJsonOptions(options => options.SerializerOptions .Converters .Add(new ValueObjectJsonConverterFactory())); ``` The code for `Newtonsoft.Json` is almost identical: ```C# var webHost = new HostBuilder() .ConfigureServices(collection => { collection.AddMvc() .AddNewtonsoftJson(options => options.SerializerSettings .Converters .Add(new ValueObjectNewtonsoftJsonConverter())); }) ``` ## MessagePack serialization * Required nuget package: [![Thinktecture.Runtime.Extensions.MessagePack](https://img.shields.io/nuget/v/Thinktecture.Runtime.Extensions.MessagePack.svg?maxAge=60&label=Thinktecture.Runtime.Extensions.MessagePack)](https://www.nuget.org/packages/Thinktecture.Runtime.Extensions.MessagePack/) There are 2 options to make the value objects *MessagePack serializable*. ### Option 1: Make project with Value Objects depend on Nuget package The easiest way is to make [Thinktecture.Runtime.Extensions.MessagePack](https://www.nuget.org/packages/Thinktecture.Runtime.Extensions.MessagePack) a dependency of the project(s) the value objects are in. The dependency doesn't have to be a direct one but can be transitive as well. The Nuget package leads to generation of a **MessagePack** formatter and flags the value object with a `MessagePackFormatterAttribute`. This way the value object can be serialized to and from **MessagePack** without extra code. ### Option 2: Register MessagePack FormatterResolver with MessagePack serializer options > For simple value objects only. If making previously mentioned Nuget package a dependency of project(s) with value objects is not possible or desirable, then the other option is to register the MessagePack formatter with *MessagePack serializer options*. By using the `ValueObjectMessageFormatterResolver` directly, the Nuget package can be installed in any project where the *MessagePack options* are configured. An example of a round-trip-serialization of the value object `ProductName`: ```C# // Use "ValueObjectMessageFormatterResolver.Instance" var resolver = CompositeResolver.Create(ValueObjectMessageFormatterResolver.Instance, StandardResolver.Instance); var options = MessagePackSerializerOptions.Standard.WithResolver(resolver); ProductName chocolate = ProductName.Create("Chocolate"); // Serialize to MessagePack var bytes = MessagePackSerializer.Serialize(chocolate, options, CancellationToken.None); // Deserialize from MessagePack var deserializedChocolate = MessagePackSerializer.Deserialize(bytes, options, CancellationToken.None); ``` ## Support for Minimal Api Parameter Binding and ASP.NET Core Model Binding Required nuget package: [![Thinktecture.Runtime.Extensions.AspNetCore](https://img.shields.io/nuget/v/Thinktecture.Runtime.Extensions.AspNetCore.svg?maxAge=60&label=Thinktecture.Runtime.Extensions.AspNetCore)](https://www.nuget.org/packages/Thinktecture.Runtime.Extensions.AspNetCore/) Having *JSON convertible* value objects is just half of the equation. If a value of a simple value object is received as a query parameter, then there is no JSON conversion in play but *ASP.NET Core Model Binding*. Besides model binding, i.e., conversion from query string to a value object, there is model validation as well. > ASP.NET Core Model Binding is for simple value objects only and for complex objects with `ValueObjectFactoryAttribute` (see section [Convert from/to any specific type](#convert-fromto-any-specific-type)). A complex value object has more than 1 property/field, so, deserialization (without `ValueObjectFactoryAttribute`) from a `string` to 2+ members is a case for JSON (de)serialization. ### Minimal Api The parameter binding of Minimal Apis in .NET 7 is still quite primitive in comparison to the model binding of MVC controllers. To make a type bindable it has to implement either `TryParse` or `BindAsync`. A simple Value Object implements `TryParse` (interface `IParsable`) by default, so it can be used with Minimal Apis without any changes. At the moment, the parameter binding (with `TryParse` and `BindAsync`) doesn't allow to pass custom validation errors to be returned to the client. The only information we can pass is an indication whether the parameter could be bound or not. ### ASP.NET Core MVC (Controllers) ASP.NET MVC gives us more control during model binding. For example, if we expect from client a `ProductName` and receive the value `A`, which is rejected by the validation, then the ASP.NET Core `ModelState` will be invalid. In this case we can reject (or let [ApiControllerAttribute](https://docs.microsoft.com/en-us/aspnet/core/web-api/?view=aspnetcore-5.0#apicontroller-attribute) reject) the request. By rejecting the request, the client gets the status code `BadRequest (400)` and the error: ```JSON { "productName": [ "Product name cannot be 1 character long." ] } ``` To help out the *Model Binding* we have to register the `ValueObjectModelBinderProvider` with ASP.NET Core. By using the custom model binder, the Nuget package can be installed in any project where ASP.NET Core is configured. > Place the "ValueObjectModelBinderProvider" before default providers, so they don't try to bind value objects. ```C# var webHost = new HostBuilder() .ConfigureServices(collection => { collection.AddMvc(options => options.ModelBinderProviders .Insert(0, new ValueObjectModelBinderProvider())); }) ``` ## Support for Entity Framework Core Optional nuget packages: [![Thinktecture.Runtime.Extensions.EntityFrameworkCore6](https://img.shields.io/nuget/v/Thinktecture.Runtime.Extensions.EntityFrameworkCore6.svg?maxAge=60&label=Thinktecture.Runtime.Extensions.EntityFrameworkCore6)](https://www.nuget.org/packages/Thinktecture.Runtime.Extensions.EntityFrameworkCore6/) [![Thinktecture.Runtime.Extensions.EntityFrameworkCore7](https://img.shields.io/nuget/v/Thinktecture.Runtime.Extensions.EntityFrameworkCore7.svg?maxAge=60&label=Thinktecture.Runtime.Extensions.EntityFrameworkCore7)](https://www.nuget.org/packages/Thinktecture.Runtime.Extensions.EntityFrameworkCore7/) [![Thinktecture.Runtime.Extensions.EntityFrameworkCore8](https://img.shields.io/nuget/v/Thinktecture.Runtime.Extensions.EntityFrameworkCore8.svg?maxAge=60&label=Thinktecture.Runtime.Extensions.EntityFrameworkCore8)](https://www.nuget.org/packages/Thinktecture.Runtime.Extensions.EntityFrameworkCore8/) [![Thinktecture.Runtime.Extensions.EntityFrameworkCore9](https://img.shields.io/nuget/v/Thinktecture.Runtime.Extensions.EntityFrameworkCore9.svg?maxAge=60&label=Thinktecture.Runtime.Extensions.EntityFrameworkCore9)](https://www.nuget.org/packages/Thinktecture.Runtime.Extensions.EntityFrameworkCore9/) Starting with Entity Framework Core 2.1 we've got the feature [Value Conversion](https://docs.microsoft.com/en-us/ef/core/modeling/value-conversions). By providing a *value converter*, EF Core can convert a simple value object (like `ProductName`) to and from a primitive type (like `string`) when persisting the data and when reading the value from database. ### Option 1: Manual registration of the ValueConverter The registration of a value converter can be done manually by using one of the method overloads of `HasConversion` in `OnModelCreating`. > This approach is not recommended because "Create" can perform some *heavy* validations which will affect performance. In general, data loaded from the database doesn't have to validated because the database is the "source of truth". Use option 2 or 3 to register a value converter which (by default) uses the constructor directly. ```C# // Entity public class Product { // other properties... public ProductName Name { get; private set; } } public class ProductsDbContext : DbContext { protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); modelBuilder.Entity(builder => { builder.Property(p => p.Name) .HasConversion(name => (string)name, s => ProductName.Create(s)); }); } } ``` Entity Framework Core value conversion is for simple value objects only. Treating a complex value object as an [owned entity](https://docs.microsoft.com/en-us/ef/core/modeling/owned-entities) or [complex type](https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-8.0/whatsnew#value-objects-using-complex-types) is more suitable than pressing multiple members into 1 column. ```C# // Entity public class Product { // other properties... public Boundary Boundary { get; private set; } } public class ProductsDbContext : DbContext { protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); modelBuilder.Entity(builder => { builder.ComplexProperty(p => p.Boundary, boundaryBuilder => { boundaryBuilder.Property(b => b.Lower).HasColumnName("Lower").HasPrecision(18, 2); boundaryBuilder.Property(b => b.Upper).HasColumnName("Upper").HasPrecision(18, 2); }); }); } } ``` ### Option 2: Registration of the ValueConverter via extension method for `ModelBuilder` Alternatively, you can install the appropriate Nuget package for [EF Core 6](https://www.nuget.org/packages/Thinktecture.Runtime.Extensions.EntityFrameworkCore6/), [EF Core 7](https://www.nuget.org/packages/Thinktecture.Runtime.Extensions.EntityFrameworkCore7/), [EF Core 8](https://www.nuget.org/packages/Thinktecture.Runtime.Extensions.EntityFrameworkCore8/) or [EF Core 9](https://www.nuget.org/packages/Thinktecture.Runtime.Extensions.EntityFrameworkCore9/) and use the extension method `AddValueObjectConverters` to register the value converters for you. ```C# public class ProductsDbContext : DbContext { protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); modelBuilder.AddValueObjectConverters(); } } ``` You can provide a delegate to adjust the configuration of [Smart Enums](Smart-Enums) and Value Objects. ```C# modelBuilder.AddValueObjectConverters( configureEnumsAndKeyedValueObjects: property => { if (property.ClrType == typeof(ProductType)) property.SetMaxLength(20); }); ``` ### Option 3: Registration of the ValueConverter via extension method for `DbContextOptionsBuilder` The other options is to use the extension method `UseValueObjectValueConverter` for the `DbContextOptionsBuilder`. ```csharp services .AddDbContext(builder => builder .UseValueObjectValueConverter(validateOnWrite: true, configureEnumsAndKeyedValueObjects: property => { if (property.ClrType == typeof(ProductType)) property.SetMaxLength(20); }) ``` ## Logging (v6.1.0 or higher) Logging can be activated in the csproj-file. Define the property `ThinktectureRuntimeExtensions_SourceGenerator_LogFilePath` pointing to an existing(!) folder (like `C:\temp\`). You can provide a file name (like `samples_logs.txt`) which is being used as a template for creation of a unique log file name like `samples_logs_20230322_220653_19c0d6c18ec14512a1acf97621912abb.txt`. > Please note, that there will be more than 1 log file (per project) because IDEs (Rider/VS) usually create 1 Source Generator for constant running in the background, and 1 for each build/rebuild of a project. Unless, `ThinktectureRuntimeExtensions_SourceGenerator_LogFilePathMustBeUnique` is set to `false`. With `ThinktectureRuntimeExtensions_SourceGenerator_LogLevel` you can specify one of the following log levels: `Trace`, `Debug`, `Information` (DEFAULT), `Warning`, `Error`. ```xml ... C:\temp\samples_logs.txt information false ``` If the logger throws an exception, for example due to insufficient file system access permissions, then the logger will try to write the exception into a temp file. You can find the file `ThinktectureRuntimeExtensionsSourceGenerator.log` in the temp folder of the user the IDE/CLI is running with. ## Real-world use cases and ideas I started to write down some examples I used in the past to show the developers the benefits of value objects and smart enums. ### Open-ended End Date There are multiple ways to implement an *end date* with open-end. All of them have their pros and cons. Here are the most popular approaches I encountered in the past: 1) Use nullable `DateOnly?` (or `DateTime?`) * **PRO**: Better semantics, i.e. `null` means there is no end date. The default value of `DateOnly?` is `null` as well, which results in expected behavior. * **CON**: (LINQ) queries must check for both `null` and a concrete date, i.e. `query.Where(i => i.MyEndDate is null || i.MyEndDate > now)`. Using such query with a database usually results in worse performance because `||`/`OR` prevents the database from using an appropriate index. 2) Use `DateOnly.MaxValue` (or `DateTime`) * **PRO**: The condition in the (LINQ) query is straight-forward `query.Where(i => i.MyEndDate > now)`. If this query is executed on a database then the database is able to use an appropriate index which result in better performance. * **CON**: Using a *special value* like `DateOnly.MaxValue` to represent an open-ended date results in worse semantics. * **CON**: The main culprit is the keyword `default` or the default value of a `DateOnly` (or `DateTime`), which is `DateOnly.MinValue`. If the property/field/variable is not assigned explicitly and stays `DateOnly.MinValue`, then this most likely will lead to an undesired behavior. In this situation I would like to have an open-ended end date instead of the date `0001-01-01`, which is an invalid end date in the most use cases. The desired solution must: * not require `OR` in queries to improve performance * have a default value which represents open-ended end date An always-valid value object `EndDate` which is a `readonly struct`. ```csharp [ValueObject(SkipKeyMember = true, // We implement the key member "Date" ourselves KeyMemberName = "Date", // Source Generator needs to know the name we've chosen DefaultInstancePropertyName = "Infinite", // "EndDate.Infinite" represent an open-ended end date EqualityComparisonOperators = OperatorsGeneration.DefaultWithKeyTypeOverloads)] // for comparison with DateOnly without implicit cast public readonly partial struct EndDate { private readonly DateOnly? _date; // can be public as well private DateOnly Date { get => _date ?? DateOnly.MaxValue; init => _date = value; } // Further validation // static partial void ValidateFactoryArguments(ref ValidationError? validationError, ref DateOnly date) // { // validationError = date.Year switch // { // < 2000 => new ValidationError("The end date lies too far in the past."), // >= 2050 => new ValidationError("The end date lies too far in the future."), // _ => validationError // }; // } } ``` Basic usage (see also [ValueObjectDemos.cs](https://github.com/PawelGerr/Thinktecture.Runtime.Extensions/blob/master/samples/Thinktecture.Runtime.Extensions.Samples/ValueObjects/ValueObjectDemos.cs)) is virtually the same as with `DateOnly` or `DateTime`. ```csharp // Create an EndDate DateOnly today = DateOnly.FromDateTime(DateTime.Now); EndDate endDate = (EndDate)today; EndDate endDate = EndDate.Create(today); // alternative // Compare the dates var isTrue = EndDate.Infinite > endDate; // Default value is equal to infinite date and equal to "DateOnly.MaxValue" var defaultEndDate = default(EndDate); var infiniteEndDate = EndDate.Infinite; isTrue = infiniteEndDate == defaultEndDate; // Get the actual date if needed DateOnly dateOfDefaultDate = defaultEndDate; DateOnly dateOfInfiniteDate = infiniteEndDate; isTrue = dateOfDefaultDate == dateOfInfiniteDate; // Compare DateOnly with EndDate isTrue = EndDate.Infinite == dateOfDefaultDate ``` Use `EndDate` with **Entity Framework Core** (see also [Product.cs](https://github.com/PawelGerr/Thinktecture.Runtime.Extensions/blob/master/samples/Thinktecture.Runtime.Extensions.EntityFrameworkCore.Samples/Product.cs), [EF-Demos](https://github.com/PawelGerr/Thinktecture.Runtime.Extensions/blob/master/samples/Thinktecture.Runtime.Extensions.EntityFrameworkCore.Samples/Program.cs#L70) and [Support for Entity Framework Core](#support-for-entity-framework-core)) > Please note that `DateOnly` is not supported in EF Core 7 but will be in EF Core 8. I use the library [ErikEJ.EntityFrameworkCore.SqlServer.DateOnlyTimeOnly](https://github.com/ErikEJ/EFCore.SqlServer.DateOnlyTimeOnly) in my EF Core 7 demos. ```csharp // Entity public class Product { ... public EndDate EndDate { get; set; } } // query var today = (EndDate)DateOnly.FromDateTime(DateTime.Today); var products = await ctx.Products .Where(p => p.EndDate >= today) .ToListAsync(); ``` Use `EndDate` with **ASP.NET Core** controllers (see also [DemoController.cs](https://github.com/PawelGerr/Thinktecture.Runtime.Extensions/blob/master/samples/Thinktecture.Runtime.Extensions.AspNetCore.Samples/Controllers/DemoController.cs)) and minimal api (see also [minimal api demo](https://github.com/PawelGerr/Thinktecture.Runtime.Extensions/blob/master/samples/Thinktecture.Runtime.Extensions.AspNetCore.Samples/Program.cs#L181)). Read the section "[Support for Minimal Api Parameter Binding and ASP.NET Core Model Binding](#support-for-minimal-api-parameter-binding-and-aspnet-core-model-binding)" to get more information. ```csharp // Controller [Route("api"), ApiController] public class DemoController : Controller { [HttpGet("enddate/{endDate}")] public IActionResult RoundTripGet(EndDate endDate) { if (!ModelState.IsValid) return BadRequest(ModelState); return Json(endDate); } [HttpPost("enddate")] public IActionResult RoundTripPost([FromBody] EndDate endDate) { if (!ModelState.IsValid) return BadRequest(ModelState); return Json(endDate); } } // Minimal api var app = builder.Build(); var routeGroup = app.MapGroup("/api"); routeGroup.MapGet("enddate/{date}", (EndDate date) => date); routeGroup.MapPost("enddate", ([FromBody] EndDate date) => date); ``` The response is the same in both cases. ``` GET api/enddate/2023-04-05 and POST api/enddate with body "2023-04-05" returns "2023-04-05" ``` ### (Always-positive) Amount Value objects are excellent for checking some kind of invariants. In one of my use cases I had to perform a calculation of moderate complexity and the result and all partial results must always be positive. We could use a plain `decimal` and check the (partial) result after every(!) arithmetic operation, but it requires more code and is difficult to read and to maintain. Instead, we switched from decimal to a `readonly struct Amount` which checks the invariant automatically. ```csharp [ValueObject(DefaultInstancePropertyName = "Zero", // renames Amount.Empty to Amount.Zero ComparisonOperators = OperatorsGeneration.DefaultWithKeyTypeOverloads, // for comparison of amount with a decimal without implicit conversion: amount > 42m AdditionOperators = OperatorsGeneration.DefaultWithKeyTypeOverloads, // for arithmetic operations of amount with a decimal without implicit conversion: amount + 42m SubtractionOperators = OperatorsGeneration.DefaultWithKeyTypeOverloads, MultiplyOperators = OperatorsGeneration.DefaultWithKeyTypeOverloads, DivisionOperators = OperatorsGeneration.DefaultWithKeyTypeOverloads)] public readonly partial struct Amount { static partial void ValidateFactoryArguments(ref ValidationError? validationError, ref decimal value) { if (value < 0) validationError = new ValidationError("Amount must be positive."); } } ``` The usage is the same as with a plain `decimal`. ```csharp // get an instance of amount with Create/TryCreate/Validate or an explicit cast var amount = Amount.Create(1m); var otherAmount = (Amount)2m; var zero = Amount.Zero; // equality comparisons amount == zero; // false amount > otherAmount; // false amount > 42m; // false amount.CompareTo(otherAmount); // -1 // arithmetic operations amount + otherAmount; // 3 amount + 42m // 43 ```