Migration from Primitive Types to Value Objects - stevedunn/vogen

Migration from Primitive Types to Value Objects

When designing software systems, it is essential to choose the right data types to represent the concepts and behaviors of the problem domain accurately. Primitive types, such as integers, strings, and booleans, are often used to model simple data structures. However, they lack the expressiveness and rich behavior required to model complex business rules and invariants. Value Objects, on the other hand, are immutable, have a well-defined behavior, and can enforce complex business rules and invariants.

In this article, we will explore the possible options for migrating from primitive types to value objects, using the project “stevedunn/vogen/” as a reference.

Option 1: Replace Primitive Types with Value Objects

The most straightforward option is to replace primitive types with value objects explicitly. This approach involves creating a new value object class that encapsulates the primitive type and provides a rich behavior that enforces the business rules and invariants.

For example, in the project “stevedunn/vogen/”, the file “samples/Vogen.Examples/Types/IntoVo.cs” shows how to replace a primitive type (string) with a value object (StringVo). The StringVo class encapsulates the string primitive type and provides a behavior that enforces the business rules and invariants.

public class StringVo
{
private readonly string _value;

public StringVo(string value)
{
if (string.IsNullOrWhiteSpace(value))
{
throw new ArgumentException("Value cannot be null or whitespace.", nameof(value));
}

_value = value;
}

public static implicit operator StringVo(string value) => new StringVo(value);

public static implicit operator string(StringVo value) => value._value;

public override string ToString() => _value;
}

To use the StringVo class, you can create an instance of it and pass it to methods or properties that expect a string value. The StringVo class will ensure that the business rules and invariants are enforced.

Option 2: Use Value Object Attributes

Another option is to use value object attributes to decorate the primitive types with metadata that defines the behavior and invariants of the value object. This approach involves creating a new attribute class that encapsulates the behavior and invariants of the value object.

For example, in the project “stevedunn/vogen/”, the file “src/Vogen.SharedTypes/ValueObjectAttribute.cs” shows how to create a ValueObjectAttribute class that can be used to decorate primitive types with metadata that defines the behavior and invariants of the value object.

[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false)]
public class ValueObjectAttribute : Attribute
{
public ValueObjectAttribute()
{
}
}

To use the ValueObjectAttribute class, you can decorate a primitive type with the attribute and define the behavior and invariants of the value object in the attribute class.

[ValueObject]
public class StringVo
{
private readonly string _value;

public StringVo(string value)
{
if (string.IsNullOrWhiteSpace(value))
{
throw new ArgumentException("Value cannot be null or whitespace.", nameof(value));
}

_value = value;
}

public static implicit operator StringVo(string value) => new StringVo(value);

public static implicit operator string(StringVo value) => value._value;

public override string ToString() => _value;
}

Option 3: Use Code Generation

A third option is to use code generation to generate the value object classes automatically from the primitive types. This approach involves creating a code generation template that can generate the value object classes based on the metadata and behavior defined in the attribute class.

For example, in the project “stevedunn/vogen/”, the file “src/Vogen/Generators/Conversions/GenerateEfCoreTypes.cs” shows how to generate the value object classes automatically from the primitive types using a code generation template.

public class GenerateEfCoreTypes
{
public static string Generate(Type type)
{
var builder = new StringBuilder();

builder.AppendLine($"public class {type.Name}Vo");
builder.AppendLine("{");
builder.AppendLine($"\tprivate readonly {type.FullName} _value;");
builder.AppendLine($"\tpublic {type.Name}Vo({type.FullName} value)");
builder.AppendLine("\t{");
builder.AppendLine($"\t\tif ({type.FullName}.IsNullOrWhiteSpace(value))");
builder.AppendLine($"\t\t{{");
builder.AppendLine($"\t\t\tthrow new ArgumentException(\"Value cannot be null or whitespace.\", nameof(value));");
builder.AppendLine($"\t\t}}");
builder.AppendLine($"\t\t_value = value;");
builder.AppendLine($"\t}");
builder.AppendLine($"\tpublic static implicit operator {type.Name}Vo({type.FullName} value) => new {type.Name}Vo(value);");
builder.AppendLine($"\tpublic static implicit operator {type.FullName}({type.Name}Vo value) => value._value;");
builder.AppendLine($"\tpublic override string ToString() => _value;");
builder.AppendLine("}");

return builder.ToString();
}
}

To use the GenerateEfCoreTypes class, you can call the Generate method and pass the primitive type as a parameter. The Generate method will return a string that contains the generated value object class.

Option 4: Use Serialization and Conversion

A fourth option is to use serialization and conversion to convert the primitive types to value objects automatically. This approach involves creating a serialization and conversion library that can convert the primitive types to value objects and vice versa.

For example, in the project “stevedunn/vogen/”, the file “src/Vogen/Templates/Short/Short_LinqToDbValueConverter.cs” shows how to create a serialization and conversion library that can convert the primitive types to value objects and vice versa using the LinqToDb library.

public class ShortValueConverter : ValueConverter<short, int>
{
public ShortValueConverter() : base(
v => (int)v,
v => (short)v)
{
}
}

To use the ShortValueConverter class, you can register it with the LinqToDb library, and it will automatically convert the primitive types to value objects and vice versa.

Conclusion

In conclusion, migrating from primitive types to value objects is an essential step in designing software systems that accurately model the problem domain. The four options presented in this article provide a range of approaches to migrating from primitive types to value objects, from simple replacement to complex code generation and serialization and conversion. By choosing the right option for your project, you can ensure that your software system accurately models the problem domain and enforces the business rules and invariants required for correct behavior.

Sources: