When it comes to programming, correctness is the name of the game. Every developer aims to understand, model, and limit potential outliers when executing code because those unknown variables can lead to exceptions and critical failures. There are many techniques developers use, but I recently discovered a new library (at least to me) in the .NET community that aims to help developers constrain potential inputs using value objects.

In this post, we’ll write a quick sample using Vogen to demonstrate how value objects can make our code clearer and less error-prone.

What is a Value Object?

A value object represents a logical concept but is a .NET primitive value such as int, bool, string, and more. A straightforward example might be a birth date. Most developers would define a birth date using the DateTime type, hoping that the variable name clarifies the value’s intention.

DateTime birthDate = new DateTime(1990, 1, 1);
C#

The drawback to this code is nothing stops a developer from unintentionally using the birthDate variable incorrectly.

DateTime birthDate = new DateTime(1990, 1, 1);
SetMovieReleaseDate(birthDate);
void SetMovieReleaseDate(DateTime date) { }
C#

While the code technically works, it may not be what the developers intended logically. The birth date may or may not be the release date of a movie, and it’s difficult to tell if this code is “correct.” Let’s fix it using value objects.

var birthDate = new BirthDateTime(new (1990, 1, 1));
var movieReleaseDate = new MovieReleaseDateTime(birthDate.Value);
SetMovieReleaseDate(movieReleaseDate);

void SetMovieReleaseDate(MovieReleaseDateTime date) { }
// value objects
public record MovieReleaseDateTime(DateTime Value);
public record BirthDateTime(DateTime Value);
C#

Looking at this code, you can see that the developer intended to set a movie release date to the same value as the birth date. The goal is to minimize the chance of autocompleting your way into logical bugs that may be difficult to track down.

Yes, this can seem overly ceremonious, so consider the benefits and drawbacks before deciding if you want to adopt it.

Now, let’s see what Vogen is about.

What is Vogen?

Vogen is a NuGet library that utilizes source generators to generate value objects, taking much of the ceremony out of model creation. The library creates factory methods, comparisons, validation, and serializers on partial struct and class implementations.

Let’s start by adding Vogen to a .NET console application.

<PackageReference Include="Vogen" Version="7.0.0-beta.1" />
XML

In my example, I’ll create a PacMan class that requires a FavoriteGhost property to have a valid value of Ghost.

public class PacMan
{
    public Ghost FavoriteGhost { get; set; }

    public override string ToString()
    {
        return $"Pac Man's favorite ghost is {FavoriteGhost}.";
    }
}
C#

Our value object will be Ghost; everyone knows that the ghosts that haunt Pac-Man include Blinky, Pinky, Inky, and Clyde. Let’s start with the unconstrained approach using Vogen.

[ValueObject<string>]
[Instance("Blinky", "Blinky")]
[Instance("Pinky", "Pinky")]
[Instance("Inky", "Inky")]
[Instance("Clyde", "Clyde")]
public partial struct Ghost;
C#

After the source generators have run, you’ll now have public static readonly instances of each ghost on the Ghost struct, allowing our code to run and set the FavoriteGhost property on our pacMan instance.

You can create any Ghost you like using the From method on the struct.

var aNewGhost = Ghost.From("Khalid");
C#

If your value objects are unconstrained, this might be a good time to stop, but we want to limit our Ghost values. Let’s rework our Ghost struct.

[ValueObject<string>]
public partial struct Ghost
{
    public static readonly Ghost Blinky = new("Blinky");
    public static readonly Ghost Pinky = new("Pinky");
    public static readonly Ghost Inky = new("Inky");
    public static readonly Ghost Clyde = new("Clyde");

    public static IReadOnlyCollection<Ghost> All { get; }
        = new[] { Blinky, Pinky, Inky, Clyde }.AsReadOnly();

    private static Validation Validate(string input) =>
        All.Any(g => g.Equals(input))
            ? Validation.Ok
            : Validation.Invalid($"Ghost must be {string.Join(", ", All)}");
}
C#

With some extra code, we can now validate that all values used to create a Ghost fit within a defined set of values.

// will throw an exception
var aNewGhost = Ghost.From("Khalid");
// will pass
var aKnownGhost = Ghost.From("Blinky");
C#

Let’s see the use of Vogen in a complete C# sample.

using Vogen;

var pacMan = new PacMan
{
    FavoriteGhost = Ghost.Blinky
};

foreach (var ghost in Ghost.All)
{
    Console.WriteLine(ghost);
}

Console.WriteLine(pacMan);

[ValueObject<string>]
public partial struct Ghost
{
    public static readonly Ghost Blinky = new("Blinky");
    public static readonly Ghost Pinky = new("Pinky");
    public static readonly Ghost Inky = new("Inky");
    public static readonly Ghost Clyde = new("Clyde");

    public static IReadOnlyCollection<Ghost> All { get; }
        = new[] { Blinky, Pinky, Inky, Clyde }.AsReadOnly();

    private static Validation Validate(string input) =>
        All.Any(g => g.Equals(input))
            ? Validation.Ok
            : Validation.Invalid($"Ghost must be {string.Join(", ", All)}");
}

public class PacMan
{
    public Ghost FavoriteGhost { get; set; }

    public override string ToString()
    {
        return $"Pac Man's favorite ghost is {FavoriteGhost}.";
    }
}
C#

Conclusion

Using value objects is an efficient way to constrain inputs and outputs logically. It helps you reflect logical constraints in the codebase and offers readability levels that could be lost using primitive types. With the addition of Vogen, you can remove boilerplate code and get straight to using value objects, with the benefits of quickly accessing the underlying primitive value through explicit and implicit means.

I think the next step for folks is likely to take an existing part of a codebase and see if converting it to use value objects improves readability and correctness.

I hope you enjoyed this post. As always, thanks for reading and sharing my posts.