C
C#•4mo ago
LazyGuard

A question about aggregates in DDD

I am watching a DDD course and the author created the following classes
public record AggregateId
{
private Guid Value { get; }

public AggregateId() : this(Guid.NewGuid())
{
}

private AggregateId(Guid value)
{
if (value == Guid.Empty)
{
throw new InvalidAggregateIdException(value);
}

Value = value;
}

public static implicit operator Guid(AggregateId id) => id.Value;
public static implicit operator AggregateId(Guid id) => new(id);

public override string ToString()
{
return Value.ToString();
}
}

public abstract class AggregateRoot
{
public AggregateId Id { get; protected set; }

// TIP: it's a good practice to include the version in the aggregate root.
// It's useful for optimistic concurrency control.
public int Version { get; protected set; }


//The order of the events matter, so we use a list.
private readonly List<IDomainEvent> _events = [];
public IEnumerable<IDomainEvent> Events => _events;


protected void AddEvent(IDomainEvent @event)
{
// The version is incremented only if there are no events in the list.
// If we changed 4 properties in out aggregate, we want to avoid change version from 1 to 5
if (_events.Count == 0)
{
Version++;
}
_events.Add(@event);
}

public void ClearEvents() => _events.Clear();
}
public record AggregateId
{
private Guid Value { get; }

public AggregateId() : this(Guid.NewGuid())
{
}

private AggregateId(Guid value)
{
if (value == Guid.Empty)
{
throw new InvalidAggregateIdException(value);
}

Value = value;
}

public static implicit operator Guid(AggregateId id) => id.Value;
public static implicit operator AggregateId(Guid id) => new(id);

public override string ToString()
{
return Value.ToString();
}
}

public abstract class AggregateRoot
{
public AggregateId Id { get; protected set; }

// TIP: it's a good practice to include the version in the aggregate root.
// It's useful for optimistic concurrency control.
public int Version { get; protected set; }


//The order of the events matter, so we use a list.
private readonly List<IDomainEvent> _events = [];
public IEnumerable<IDomainEvent> Events => _events;


protected void AddEvent(IDomainEvent @event)
{
// The version is incremented only if there are no events in the list.
// If we changed 4 properties in out aggregate, we want to avoid change version from 1 to 5
if (_events.Count == 0)
{
Version++;
}
_events.Add(@event);
}

public void ClearEvents() => _events.Clear();
}
18 Replies
LazyGuard
LazyGuard•4mo ago
public class Resource : AggregateRoot
{
public IEnumerable<string> Tags { get; }
public IEnumerable<Reservation> Reservations { get; }

private Resource(AggregateId id, IEnumerable<string> tags, IEnumerable<Reservation>? reservations = null, int version = 0)
{
var tagsSet = tags.ToHashSet();
ValidateTags(tagsSet);

Id = id;
Tags = tagsSet;
Reservations = reservations?.ToHashSet() ?? [];
Version = version;
}
public static Resource Create(AggregateId id, IEnumerable<string> tags, IEnumerable<Reservation>? reservations = null)
{
var resource = new Resource(id, tags, reservations);
resource.AddEvent(new ResourceCreated(resource));
return resource;
}

private static void ValidateTags(HashSet<string> tags)
{
if (tags is null || tags.Count == 0)
{
// TIP: stick to custom exceptions. This is more expressive than using built-in exception, it gives more context.
// It pays off for testing.
// We will see later how plugin a middleware to map those to status codes.
throw new MissingResourceTagsException();
}

if (tags.Any(string.IsNullOrWhiteSpace))
{
throw new InvalidResourceTagsException();
}
}
}
public class Resource : AggregateRoot
{
public IEnumerable<string> Tags { get; }
public IEnumerable<Reservation> Reservations { get; }

private Resource(AggregateId id, IEnumerable<string> tags, IEnumerable<Reservation>? reservations = null, int version = 0)
{
var tagsSet = tags.ToHashSet();
ValidateTags(tagsSet);

Id = id;
Tags = tagsSet;
Reservations = reservations?.ToHashSet() ?? [];
Version = version;
}
public static Resource Create(AggregateId id, IEnumerable<string> tags, IEnumerable<Reservation>? reservations = null)
{
var resource = new Resource(id, tags, reservations);
resource.AddEvent(new ResourceCreated(resource));
return resource;
}

private static void ValidateTags(HashSet<string> tags)
{
if (tags is null || tags.Count == 0)
{
// TIP: stick to custom exceptions. This is more expressive than using built-in exception, it gives more context.
// It pays off for testing.
// We will see later how plugin a middleware to map those to status codes.
throw new MissingResourceTagsException();
}

if (tags.Any(string.IsNullOrWhiteSpace))
{
throw new InvalidResourceTagsException();
}
}
}
Here what's the value of having an Id that just Wraps the guid ? why not use a GUID directly as simple as this directly in the Resource class Why all this "boilerplate" ? What's the purpose to inherit from an AggregateRoot ``` I am lost 😅
Joschi
Joschi•4mo ago
You generally use strongly typed IDs to prevent confusion later on. Imagine a method in which a user edits a comment under some post. That needs the id of the user, the id of the post he is editing and the id of the comment and of course the new content. Now the signature would be Guid, Guid, Guid, string. It is very easy to make a mistake here. Like passing the userId as a commentId. If you use strongly typed IDs, your signature becomes. UserId, PostId, CommentId, string. Now you cannot mess this up, because the compiler will stop you.
Jimmacle
Jimmacle•4mo ago
also because DDD is tons of boilerplate
LazyGuard
LazyGuard•4mo ago
but if the user is an aggrgate, and the post is an aggregate and the comment is an aggregate, the signature would be AggregateId, AggregateId, AggregateId, string right ?
Joschi
Joschi•4mo ago
No
LazyGuard
LazyGuard•4mo ago
because all of three will inherit from AggregateRoot
Joschi
Joschi•4mo ago
Generally you wouldn't do that. But in this example yes. But those three wouldn't all be aggregateRoots
Jimmacle
Jimmacle•4mo ago
is there a reason you're trying to use/learn DDD? it's generally a mess that you shouldn't bother with unless forced
Joschi
Joschi•4mo ago
DDD is a lot of boilerplate that may pay off in the end, but for any smaller projects it's probably not worth sticking to it.
LazyGuard
LazyGuard•4mo ago
I am forced to for an interview yes, unfortunately -_- but I am untrested to learn about simpler stuff as well any suggestions on materials about architecture ad design but that are simpler ?
Jimmacle
Jimmacle•4mo ago
$vsa is generally recommended over DDD/CA here
Joschi
Joschi•4mo ago
I would use a generic Interface for the AggregateRoot. Like AggregateRoot<TId> Architecture is very hard to learn, without trying out a bunch of stuff and experiencing the advantages and disadvantages of them all.
LazyGuard
LazyGuard•4mo ago
@Joschi why making it generic ? any advantages ?
Mayor McCheese
Mayor McCheese•4mo ago
I mean, if you don't want to do DDD, not for nothing here, don't interview for a DDD position.
Joschi
Joschi•4mo ago
The advantage is, that you can use a different strongly typed ID for each AggregateRoot instead of having a single typed ID be used multiple times, which kind of defeats the purpose of strongly typed IDs imo.
LazyGuard
LazyGuard•4mo ago
that's in theory, in practice I need to eat. Contrary to USA, DDD is still a huge thing in Europe, unfortunatel @Joschi thanks !
phaseshift
phaseshift•4mo ago
Is it? There might also be some conflating of DDD and event sourcing going on here. But I imagine DDD when applied often goes into the cqrs and/or event sourcing just because of the available material