Events change state

If we move on to the idea of event-sourcing, events represent the fact of state change. It means that an entity state cannot be changed without some interaction with a domain event. But in our code so far the fact of changing the system state and raising a domain event is completely separated. Let's see how we can change it.

First, we need to make some changes in the Entity base class:

using System.Collections.Generic;
using System.Linq;

namespace Marketplace.Framework
{
public abstract class Entity
{
private readonly List<object> _events;

protected Entity() => _events = new List<object>();

protected void Apply(object @event)
{
When(@event);
EnsureValidState();
_events.Add(@event);
}

protected abstract void When(object @event);

public IEnumerable<object> GetChanges() => _events.AsEnumerable();

public void ClearChanges() => _events.Clear();

protected abstract void EnsureValidState();
}
}

We have renamed the Raise method to Apply since it will not only add events to the list of changes but physically apply the content of each event to the entity state. We do it by using the When method, which each entity needs to implement. The Apply method also calls the EnsureValidState method, which we previously had in the entity but not in the base class. By doing this, we remove the need to call this method for each operation on the entity.

The next step would be to apply domain events and move all state changes to the When method.

using Marketplace.Framework;

namespace Marketplace.Domain
{
public class ClassifiedAd : Entity
{
public ClassifiedAdId Id { get; private set; }
public UserId OwnerId { get; private set; }
public ClassifiedAdTitle Title { get; private set; }
public ClassifiedAdText Text { get; private set; }
public Price Price { get; private set; }
public ClassifiedAdState State { get; private set; }
public UserId ApprovedBy { get; private set; }

public ClassifiedAd(ClassifiedAdId id, UserId ownerId) =>
Apply(new Events.ClassifiedAdCreated
{
Id = id,
OwnerId = ownerId
});

public void SetTitle(ClassifiedAdTitle title) =>
Apply(new Events.ClassifiedAdTitleChanged
{
Id = Id,
Title = title
});

public void UpdateText(ClassifiedAdText text) =>
Apply(new Events.ClassifiedAdTextUpdated
{
Id = Id,
AdText = text
});

public void UpdatePrice(Price price) =>
Apply(new Events.ClassifiedAdPriceUpdated
{
Id = Id,
Price = price.Amount,
CurrencyCode = price.Currency.CurrencyCode
});

public void RequestToPublish() =>
Apply(new Events.ClassidiedAdSentForReview {Id = Id});

protected override void When(object @event)
{
switch (@event)
{
case Events.ClassifiedAdCreated e:
Id = new ClassifiedAdId(e.Id);
OwnerId = new UserId(e.OwnerId);
State = ClassifiedAdState.Inactive;
break;
case Events.ClassifiedAdTitleChanged e:
Title = new ClassifiedAdTitle(e.Title);
break;
case Events.ClassifiedAdTextUpdated e:
Text = new ClassifiedAdText(e.AdText);
break;
case Events.ClassifiedAdPriceUpdated e:
Price = new Price(e.Price, e.CurrencyCode);
break;
case Events.ClassidiedAdSentForReview e:
State = ClassifiedAdState.PendingReview;
break;
}
}

protected override void EnsureValidState()
{
bool valid = Id != null && OwnerId != null;
switch (State)
{
case ClassifiedAdState.PendingReview:
valid = valid
&& Title != null
&& Text != null
&& Price?.Amount > 0;
break;
case ClassifiedAdState.Active:
valid = valid
&& Title != null
&& Text != null
&& Price?.Amount > 0
&& ApprovedBy != null;
break;
}

if (!valid)
throw new InvalidEntityStateException(this,
$"Post-checks failed in state {State}");
}

public enum ClassifiedAdState
{
PendingReview,
Active,
Inactive,
MarkedAsSold
}
}
}

There are two essential things that we changed in the entity class:

  • All public methods to amend entity state (operations) now apply to domain events. There are no state changes or validity checks left in those methods. As you remember, the validity contract method is now being called from the Apply method in the Entity base class.
  • We have added a When method override, where the advanced pattern matching feature of C# 7.1 is being used to identify, what kind of event is being applied and how the entity state needs to be changed.

Hence there are no changes in tests. If we execute all tests in the solution that have been created so far, they will all pass. It means that raising domain events and applying them to change the entity state can be considered as implementation details. Indeed, this is a style of working with domain events, typically used when DDD is applied with event-sourcing, which we will be discussing later.

Please keep in mind that using Domain-Driven Design in general and domain events, in particular, does not imply using event-sourcing, and vice versa. But this book has more focus on event-sourcing. Therefore this technique to change the state of the domain by applying events is presented quite early.

Some more changes are not that obvious, which were required to make the whole thing work. If you look close to the When method, entity properties that are still of value object types, use constructors for value objects instead of factory functions. It is because factory functions apply constraints and perform checks while constructing valid value objects. But domain events represent something that already happened, so there is no point in checking these past facts for validity. If they were valid at a time, they should be just let through. Even if the logic in value object has changed, this should never have any effects on applying events with historical data.

To fix this, we needed to change value objects, so they have internal constructors instead of private ones. Also, checks are moved from constructors to factory functions, so constructors are now accepting any value. For the more complex Price object, we needed to add a constructor that does not require currency lookup service. Even if the currency is not valid anymore when we are trying to load some past event, it should get through. It, however, does not change the use of factory functions. They still require the lookup service and will be using it as soon as we create new instances if the value objects in our application service layer. It will keep protecting us from executing commands that have some incorrect information and therefore can bring our model to an invalid state.

Below you can find changed value objects.

using Marketplace.Framework;

namespace Marketplace.Domain
{
public class ClassifiedAdText : Value<ClassifiedAdText>
{
public string Value { get; }

internal ClassifiedAdText(string text) => Value = text;

public static ClassifiedAdText FromString(string text) =>
new ClassifiedAdText(text);

public static implicit operator string(ClassifiedAdText text) =>
text.Value;
}
}
using System;
using System.Text.RegularExpressions;
using Marketplace.Framework;

namespace Marketplace.Domain
{
public class ClassifiedAdTitle : Value<ClassifiedAdTitle>
{
public static ClassifiedAdTitle FromString(string title)
{
CheckValidity(title);
return new ClassifiedAdTitle(title);
}

public static ClassifiedAdTitle FromHtml(string htmlTitle)
{
var supportedTagsReplaced = htmlTitle
.Replace("<i>", "*")
.Replace("</i>", "*")
.Replace("<b>", "**")
.Replace("</b>", "**");

var value = Regex.Replace(supportedTagsReplaced, "<.*?>", string.Empty);
CheckValidity(value);

return new ClassifiedAdTitle(value);
}

public string Value { get; }

internal ClassifiedAdTitle(string value) => Value = value;

public static implicit operator string(ClassifiedAdTitle title) =>
title.Value;

private static void CheckValidity(string value)
{
if (value.Length > 100)
throw new ArgumentOutOfRangeException(
"Title cannot be longer that 100 characters",
nameof(value));
}
}
}
using System;

namespace Marketplace.Domain
{
public class Price : Money
{
private Price(decimal amount, string currencyCode, ICurrencyLookup currencyLookup)
: base(amount, currencyCode, currencyLookup)
{
if (amount < 0)
throw new ArgumentException(
"Price cannot be negative",
nameof(amount));
}

internal Price(decimal amount, string currencyCode)
: base(amount, new CurrencyDetails{CurrencyCode = currencyCode})
{
}

public static Price FromDecimal(decimal amount, string currency,
ICurrencyLookup currencyLookup) =>
new Price(amount, currency, currencyLookup);
}
}

Again, although these changes might seem significant, we were not changing any domain logic and constraints. We have all existing tests intact, and they are still passing, so our refactoring was successful, and we managed to changed implementation details while keeping the essence of our domain model intact.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.14.142.194