Vogen

repository·main·Indexed 23 days ago

https://github.com/stevedunn/vogen

A C# source generator and code analyzer designed to eliminate 'Primitive Obsession' by creating strongly-typed Value Objects. Vogen provides validation, prevents accidental type mixing, and handles boilerplate for serialization and type conversion. It supports integration with System.Text.Json, Newtonsoft.Json, Dapper, EFCore, LINQ to DB, and protobuf-net, ensuring domain safety by prohibiting the use of 'new' or 'default' keywords for value object instantiation.

Tokens
32.2K
Snippets
90
Records
141
Agent score
77%

What's inside Vogen

  1. Understand the limitations of using records with Vogen

    main

    If you choose to use record instead of class or struct, be aware of the following implementation differences in the generated code:

    1. Value property accessibility: For records, the generated Value property uses init accessibility to support C# with expressions (e.g., var vo2 = vo1 with { Value = 42 }). However, using with to change the value does not correctly trigger the internal 'initialized' state of the object. While the Vogen analyzer will still catch this and cause a compilation error, it is technically inconsistent with the intended lifecycle.
    2. ToString implementation: Vogen explicitly overrides ToString for classes and structs to avoid the default record behavior (which enumerates all fields). For records, Vogen still performs this override to ensure the output remains controlled.
  2. Prohibiting default construction of Value Object structs

    main

    Vogen includes analyzers that prevent the use of default or new() (without arguments) for Value Object structs to ensure invalid data cannot enter the domain. This triggers a compilation error VOG009.

    // error VOG009: Type 'CustomerId' cannot be constructed with default as it is prohibited.
    CustomerId c = default;
    
    // error VOG009: Type 'CustomerId' cannot be constructed with default as it is prohibited.
    var c2 = default(CustomerId);
  3. Constraints on custom constructors for Value Object structs

    main

    When using struct as the underlying type for a Value Object, you cannot define your own constructors. Vogen automatically generates:

    1. A parameter-less constructor (private).
    2. A constructor that takes the underlying value.

    Attempting to add custom constructors will result in compilation errors (e.g., VOG008). Use the generated .From() method for creation instead.

  4. Use records with Vogen

    main

    Vogen supports both record class and record struct.

    Recommendation: It is generally recommended to use a vanilla class or struct instead of records. Because Vogen's purpose is to wrap and protect a single primitive value, the standard benefits of C# records (like automatic equality and boilerplate generation) do not provide significant additional value for Vogen-wrapped types.

    If you choose to use records, be aware of the following behaviors:

    • GetHashCode(): Vogen does not generate a custom GetHashCode() implementation because the compiler's default implementation for records is sufficient.
    • Equals: Vogen does not generate Equals overloads (e.g., Equals(vo left, vo right)) because the compiler handles this automatically for records.
    • ToString: Vogen overrides ToString. For record class, the default compiler implementation enumerates fields and properties, which can cause issues during deserialization or conversion if the type is not fully initialized.
    • With expressions: The with keyword is supported. Using with will correctly trigger Vogen's normalization and validation logic.
    • Primary Constructors: Primary constructors are not supported. Using them breaks Vogen's core constraint that all value objects must be created via the From method.
  5. Hoisting IConvertible implementation

    main

    If the underlying primitive implements IConvertible, the generated wrapper will also implement IConvertible. This enables compatibility with Convert.ChangeType(), ORMs, and reflection-based serialization. The generated methods (e.g., ToInt32(), ToDecimal(), ToBoolean()) delegate directly to the underlying primitive.

    Customization: You can provide your own custom implementations for specific IConvertible methods. Vogen will respect your custom code and only hoist the remaining methods that you haven't implemented.

  6. Hoisting IComparable implementation

    main

    If the underlying primitive implements IComparable<>, you can configure Vogen to implement IComparable<> and IComparable on the generated wrapper.

    To enable this, set the ComparisonGeneration configuration to UseUnderlying. The generated method will be public int CompareTo([primitive] other)... where the generic argument is the type of the wrapped primitive.

  7. Register BSON serializers for MongoDB

    main

    Once serializers are generated, they must be registered with the MongoDB BsonSerializer. Vogen automatically generates a static class named RegisterBsonSerializersFor[NameOfProject] to handle this.

    Automatic Registration (Default)

    By default, the generated static class contains a static constructor that automatically registers all BSON serializers the first time the class is accessed.

    Manual Registration

    If you need to perform BSON configuration before the serializers are registered, you can switch to manual registration mode using the VogenDefaults assembly attribute with the Customizations.ManuallyRegisterBsonSerializers flag. This prevents the automatic static constructor from running and instead provides a TryRegister() method for you to call explicitly.

  8. How IConvertible is automatically generated for Value Objects

    main

    When a Vogen ValueObject is based on a primitive type that implements System.IConvertible (such as int, float, decimal, DateTime, etc.), Vogen automatically generates the IConvertible interface on the value object.

    This allows your value objects to integrate seamlessly with .NET frameworks and APIs that rely on runtime type conversion, including:

    • Convert.ChangeType() for dynamic conversions.
    • ORMs (like Entity Framework or Dapper) that map database columns to properties at runtime.
    • Reflection-based frameworks like ASP.NET Core parameter binding or WPF data binding.
    • Generic data transformation pipelines.
    [ValueObject<int>]
    public partial struct UserId { }
    
    var userId = UserId.From(42);
    // IConvertible enables Convert.ChangeType to work directly with the value object
    object asString = Convert.ChangeType(userId, typeof(string));  // "42"
  9. Identify Vogen-generated types via GeneratedCodeAttribute

    main

    You can programmatically identify if a type was generated by Vogen by checking for the GeneratedCodeAttribute and verifying that the Tool property is set to "Vogen". This is useful for automation, such as automatically applying EF Core ValueConverters to all Vogen value objects in an assembly.

    // Helper class to identify Vogen types
    internal static class AttributeHelper
    {
        public static bool IsVogenValueObject(this Type targetType)
        {
            Maybe<GeneratedCodeAttribute> generatedCodeAttribute = 
                targetType.GetClassAttribute<GeneratedCodeAttribute>();
            
            return generatedCodeAttribute.HasValue && 
                generatedCodeAttribute.Value.Tool == "Vogen";
        }
    
        private static Maybe<TAttribute> GetClassAttribute<TAttribute>(
            this Type targetType) where TAttribute : Attribute
        {
            return targetType.GetAttribute<TAttribute>();
        }
    }
  10. Understand Primitive Obsession and Value Objects

    main

    Primitive Obsession (also known as 'StringlyTyped') is a code smell where primitives like int or string are used to represent domain objects (e.g., a CustomerId).

    Using primitives leads to several issues:

    1. Lack of Constraints: Primitives cannot enforce domain rules (e.g., a CustomerId should not be negative).
    2. Validation Overhead: Because primitives don't guarantee validity, you must re-validate them every time they are used.
    3. Type Confusion: Primitives allow accidental comparisons between unrelated domain concepts (e.g., comparing a SupplierId of 42 to a CustomerId of 42), which will evaluate to true if they are both just int values.
    4. Parameter Misordering: Methods accepting multiple primitives (e.g., int customerId, int supplierId) are prone to caller errors where arguments are swapped without compiler warnings.

    Vogen solves this by allowing you to define Value Objects. These types wrap primitives to enforce constraints, provide type safety at compile time, and ensure that domain logic is expressed through domain language rather than generic C# types.

  11. Constraints on custom constructors in Vogen structs

    main

    When defining a struct Value Object, you cannot define your own constructors. Vogen automatically generates the parameter-less constructor and the constructor that accepts the underlying value. Attempting to add custom constructors will result in compilation errors (e.g., VOG008).

    [ValueObject(typeof(int))]
    public partial struct CustomerId {
        // This will cause a compilation error:
        public CustomerId(int value) { }
    }