Eduard Keilholz

Hi, my name is Eduard Keilholz. I'm a Microsoft developer working at 4DotNet in The Netherlands. I like to speak at conferences about all and nothing, mostly Azure (or other cloud) related topics.
LinkedIn | Twitter | Mastodon | Bsky


I received the Microsoft MVP Award for Azure

Eduard Keilholz
HexMaster's Blog
Some thoughts about software development, cloud, azure, ASP.NET Core and maybe a little bit more...

Value Objects of DDD

So now you had an introduction to DDD you’ve probably gotten enthusiastic and started right away, of course! And then you ran into a couple of problems. As mentioned, your domain model must (always) be in a valid state. However, for some reason you were not able to.

Let’s for example, create a domain model for an appointment:

public sealed class Appointment
{
    public Guid Id { get; }
    public string Title { get; private set; }
    public string Description { get; private set; }
    public DateTimeOffset StartsOn { get; private set; }
    public DateTimeOffset EndsOn { get; private set; }

    public void SetTitle(string value)
    {
        if (!String.IsNullOrEmpty(value))
        {
            Title = value;
        }
    }
    public void SetDescription(string value)
    {
        Description = value;
    }

    public void SetStartDate(DateTimeOffset value)
    {
        StartsOn = value;
    }
    public void SetEndDate(DateTimeOffset value)
    {
        EndsOn = value;
    }

    public Appointment(string title, DateTimeOffset start, DateTimeOffset end)
    {
        Id = Guid.NewGuid();
        SetTitle(title);
        SetStartDate(start);
        SetEndDate(end);
    }
    public Appointment(Guid id, string title, string description, DateTimeOffset start, DateTimeOffset end)
    {
        Id = id;
        Title = title;
        Description = description;
        StartsOn = start;
        EndsOn = end;
    }
}

Pretty straight forward right? There’s a nice couple of properties, and a constructor accepting a title, start- and end date.

Now the problem here is that as soon as the domain model is instantiated, it may be invalid. This is because an appointment has a start- and end date on which validation rules apply. But in this situation, there’s no way I can validate the start and end date. You can validate the start- and end date in the constructor, because both the start and the end date are passed to the constructor. The problem is, when editing the appointment.

The user made a mistake entering the appointment and needs to change the start- and end date of the appointment. When you fetch the domain model from a data store and start changing the dates, there’s no way to verify if a value is valid or not. If you pass the start date, it’s validated against an old end date and vice versa. In this situation, you want to use value objects.

Let’s refactor the above domain model so it holds a value object with which we can make sure the domain model is always valid. First, we create a value object called DateRange. Take a peek at the following code :

public sealed class Appointment
{
    public Guid Id { get; }
    public string Title { get; private set; }
    public string Description { get; private set; }
    public DateRange Schedule { get; private set; }

    public void SetTitle(string value)
    {
        if (!String.IsNullOrEmpty(value))
        {
            Title = value;
        }
    }
    public void SetDescription(string value)
    {
        Description = value;
    }

    public void SetSchedule(DateRange value)
    {
        if (value.RangeStart.CompareTo(value.RangeEnd) < 0)
        {
            Schedule = value;
        }
    }

    public Appointment(string title, DateTimeOffset start, DateTimeOffset end)
    {
        Id = Guid.NewGuid();
        SetTitle(title);

        var schedule = new DateRange(start, end);
        SetSchedule(schedule);
    }
    public Appointment(Guid id, 
    string title, 
    string description, 
    DateRange schedule)
    {
        Id = id;
        Title = title;
        Description = description;
        Schedule = schedule;
    }
}

public sealed class DateRange
{
    public readonly DateTimeOffset RangeStart { get; }
    public readonly DateTimeOffset RangeEnd { get; }

    public DateRange(DateTimeOffset start, DateTimeOffset end)
    {
        RangeStart = start;
        RangeEnd = end;
    }
}

The DateRange object holds a Start- and an End date for a date range. The Appointment Domain Model is changed, so it holds a DateRange object property named Schedule. Now if a user wants the change the appointment, you create a new DateRange object containing the start- and the end date and pass it to the SetSchedule method. This method accepts the DateRange object and validates if the start date is actually earlier than the end date. And now everybody is happy, and our domain model is always valid.